fix(bib,llm): rule kind from the rule's own text — proposed rules were labelled 'final' because CMS titles since CY2018 carry no kind word (refs #691 #692)
Some checks failed
CI / lint (push) Successful in 29s
CI / notebooks-smoke (push) Successful in 1m27s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
CI / test (push) Has been cancelled
Deploy / report (push) Has been cancelled
Infra CI / zotero (push) Has been cancelled
Infra CI / docs (push) Has been cancelled
Infra CI / api (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
Infra CI / notebooks (push) Has been cancelled
Some checks failed
CI / lint (push) Successful in 29s
CI / notebooks-smoke (push) Successful in 1m27s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
CI / test (push) Has been cancelled
Deploy / report (push) Has been cancelled
Infra CI / zotero (push) Has been cancelled
Infra CI / docs (push) Has been cancelled
Infra CI / api (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
Infra CI / notebooks (push) Has been cancelled
bib.frlink.rule_kind decides proposed/final/correction from the FR paragraphs
("this proposed rule"/"we propose" vs "this final rule"/"we are finalizing";
a Correction title wins outright); the title is only the fallback and an
undecidable rule is labelled 'rule', never 'final'. All 80 grabbed PFS rules
classify correctly (30 proposed, 28 final, 22 corrections) — ten post-2017
proposed rules (incl. the CY2027 NPRM 5ITGVDJV and XFGGRBDH) were 'final' before.
Lineage labels, the collapse tie-break and the golden audio-only label use it.
This commit is contained in:
@@ -434,6 +434,83 @@ def resolve(
|
||||
# ── Transmutations + rendering ──────────────────────────────────────
|
||||
|
||||
|
||||
RULE_KINDS = ("proposed", "final", "correction", "rule")
|
||||
|
||||
# A rule's own body text says what it is: a proposed rule refers to itself
|
||||
# as "this proposed rule" and says "we propose"/"we are proposing"; a final
|
||||
# rule says "this final rule" and "we are finalizing"/"we finalized" (and
|
||||
# "we proposed", past tense, which these patterns deliberately exclude).
|
||||
_PROPOSED_SIGNALS = (
|
||||
"%this proposed rule%",
|
||||
"%we propose %",
|
||||
"%we are proposing%",
|
||||
)
|
||||
_FINAL_SIGNALS = (
|
||||
"%this final rule%",
|
||||
"%we are finalizing%",
|
||||
"%we finalized%",
|
||||
)
|
||||
|
||||
|
||||
def _signal_count(con: Any, item_key: str, patterns: tuple[str, ...]) -> int:
|
||||
where = " OR ".join("text LIKE ?" for _ in patterns)
|
||||
row = con.execute(
|
||||
f"SELECT count(*) FROM fr_anchors WHERE item_key = ? AND ({where})", # noqa: S608
|
||||
(item_key, *patterns),
|
||||
).fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
|
||||
def kind_from_title(title: str) -> str:
|
||||
"""``proposed`` / ``final`` / ``correction`` from a rule's title, else
|
||||
``rule``. Titles are the FALLBACK only — since CY2018 the CMS titles
|
||||
("Medicare and Medicaid Programs; CY 2027 Payment Policies Under the
|
||||
Physician Fee Schedule …") carry no proposed/final word at all."""
|
||||
t = (title or "").lower()
|
||||
if "correction" in t or "correcting amendment" in t:
|
||||
return "correction"
|
||||
if "proposed" in t:
|
||||
return "proposed"
|
||||
if "final" in t:
|
||||
return "final"
|
||||
return "rule"
|
||||
|
||||
|
||||
def rule_kind(store: Store, item_key: str, *, title: str | None = None) -> str:
|
||||
"""What kind of FR document *item_key* is: ``proposed``, ``final``,
|
||||
``correction`` or ``rule`` (undecidable — never claim ``final``
|
||||
without evidence).
|
||||
|
||||
A title that says "Correction" wins outright — a correction notice
|
||||
quotes the rule it corrects, so its text is not evidence of its
|
||||
own kind. Otherwise the rule's OWN paragraphs decide
|
||||
(``fr_anchors``): the proposed/final self-reference counts are
|
||||
decisive when one is at least three times the other. Measured on
|
||||
the 80 grabbed PFS rules (2001–2026) every proposed and final rule
|
||||
is decisive this way, including the ten post-2017 proposed rules
|
||||
whose titles never say "Proposed". Short notices with no
|
||||
self-references fall back to the title (``kind_from_title``).
|
||||
|
||||
*title* may be passed by a caller that already holds the item
|
||||
(saves the ``store.get`` round trip); ``None`` fetches it, and an
|
||||
unknown item is ``rule``."""
|
||||
if title is None:
|
||||
try:
|
||||
title = store.get(item_key).title
|
||||
except Exception: # noqa: BLE001 — unknown item: undecidable, not "final"
|
||||
title = ""
|
||||
if kind_from_title(title) == "correction":
|
||||
return "correction"
|
||||
con = store._con() # noqa: SLF001
|
||||
proposed = _signal_count(con, item_key, _PROPOSED_SIGNALS)
|
||||
final = _signal_count(con, item_key, _FINAL_SIGNALS)
|
||||
if proposed and proposed >= 3 * final:
|
||||
return "proposed"
|
||||
if final and final >= 3 * proposed:
|
||||
return "final"
|
||||
return kind_from_title(title)
|
||||
|
||||
|
||||
def page_of(store: Store, item_key: str, p_id: int) -> int:
|
||||
"""The printed FR page a paragraph anchor sits on."""
|
||||
row = _anchor_row(store._con(), item_key, p_id) # noqa: SLF001
|
||||
|
||||
@@ -299,15 +299,30 @@ class LineageEvidence:
|
||||
|
||||
|
||||
def rule_label(
|
||||
title: str, date_published: str, p_id: int, *, vol: str = "", page: int = 0
|
||||
title: str,
|
||||
date_published: str,
|
||||
p_id: int,
|
||||
*,
|
||||
vol: str = "",
|
||||
page: int = 0,
|
||||
kind: str = "",
|
||||
) -> str:
|
||||
"""``"CY2021 PFS final 85 FR 84639 ¶1578"`` when *vol*/*page* are
|
||||
known (Ruling B13 — ``I2``), else the shorter
|
||||
``"CY2021 PFS final ¶1578"`` / ``"CY2027 PFS proposed ¶394"``.
|
||||
``proposed`` when *title* contains "Proposed" (case-insensitive),
|
||||
else ``final``; the year from ``rule_year_of``."""
|
||||
|
||||
*kind* is ``proposed`` / ``final`` / ``correction`` / ``rule`` as
|
||||
decided by ``bib.frlink.rule_kind`` from the rule's own text (the
|
||||
chat passes it via ``_rule_kind``). Without it the title decides
|
||||
(``bib.frlink.kind_from_title``) — and a title that says neither
|
||||
labels the row ``rule``, never ``final``: since CY2018 the CMS
|
||||
titles carry no proposed/final word, and a proposed rule presented
|
||||
as final is a false statement of policy. The year comes from
|
||||
``rule_year_of``."""
|
||||
from bib.frlink import kind_from_title # bib is an optional extra
|
||||
|
||||
year = rule_year_of(title, date_published)
|
||||
kind = "proposed" if "proposed" in (title or "").lower() else "final"
|
||||
kind = kind or kind_from_title(title)
|
||||
if vol and page:
|
||||
return f"CY{year} PFS {kind} {vol} FR {page} ¶{p_id}"
|
||||
return f"CY{year} PFS {kind} ¶{p_id}"
|
||||
@@ -377,6 +392,10 @@ class _ReplicaCache:
|
||||
_ITEM_CACHE = _ReplicaCache()
|
||||
_URL_CACHE = _ReplicaCache()
|
||||
_PARAGRAPH_CACHE = _ReplicaCache()
|
||||
#: item_key -> "proposed"/"final"/"correction"/"rule" (bib.frlink.rule_kind),
|
||||
#: keyed on the bib mtime like the item cache: the answer reads the rule's
|
||||
#: paragraphs, so a re-grab can change it.
|
||||
_KIND_CACHE = _ReplicaCache()
|
||||
#: code -> on-demand EventRow list — keyed on the DuckDB REPLICA's mtime
|
||||
#: (passed explicitly by callers): on-demand lineage is derived from
|
||||
#: ``pfs.rvu``/``pfs.code_event``, which live on the replica, not the
|
||||
@@ -431,7 +450,36 @@ def _source_label(store: Any, item_key: str, p_id: int, mtime: int) -> str:
|
||||
if para is not None:
|
||||
vol = str(para.get("fr_volume") or "")
|
||||
page = para.get("page") or 0
|
||||
return rule_label(item.title, item.date_published, p_id, vol=vol, page=page)
|
||||
return rule_label(
|
||||
item.title,
|
||||
item.date_published,
|
||||
p_id,
|
||||
vol=vol,
|
||||
page=page,
|
||||
kind=_rule_kind(store, item_key, mtime),
|
||||
)
|
||||
|
||||
|
||||
def _rule_kind(store: Any, item_key: str, mtime: int) -> str:
|
||||
"""``bib.frlink.rule_kind`` for *item_key*, cached per item on the
|
||||
bib mtime; ``rule`` (undecided) when the store can't answer — a
|
||||
label must never say ``final`` on a failure path."""
|
||||
if store is None or not item_key:
|
||||
return "rule"
|
||||
cached = _KIND_CACHE.get(item_key, mtime)
|
||||
if cached is not _MISSING:
|
||||
return cached
|
||||
try:
|
||||
from bib.frlink import rule_kind
|
||||
|
||||
item = _cached_item(store, item_key, mtime) # one Store.get per item
|
||||
title = (item.title or "") if item is not None else ""
|
||||
kind = rule_kind(store, item_key, title=title)
|
||||
except Exception as e: # noqa: BLE001
|
||||
log.warning("lineage rule kind unresolved (%s): %s", item_key, e)
|
||||
kind = "rule"
|
||||
_KIND_CACHE.put(item_key, mtime, kind)
|
||||
return kind
|
||||
|
||||
|
||||
def _fr_url(store: Any, item_key: str, p_id: int, mtime: int) -> str:
|
||||
@@ -499,16 +547,12 @@ def _resolve_prompt_urls(
|
||||
|
||||
|
||||
def _is_proposed(store: Any, item_key: str, mtime: int) -> bool:
|
||||
"""True when *item_key*'s title contains "proposed" — ``False`` (not
|
||||
"unknown") for an empty *item_key* or a *store*-less caller, which
|
||||
"""True when ``bib.frlink.rule_kind`` decides *item_key* is a
|
||||
proposed rule (from the rule's own text, title as fallback) —
|
||||
``False`` for an empty *item_key* or a *store*-less caller, which
|
||||
keeps every ``_pref_key`` caller that doesn't have a store (existing
|
||||
tests, mostly) exactly as deterministic as before Ruling B12."""
|
||||
if store is None or not item_key:
|
||||
return False
|
||||
item = _cached_item(store, item_key, mtime)
|
||||
if item is None:
|
||||
return False
|
||||
return "proposed" in (item.title or "").lower()
|
||||
return _rule_kind(store, item_key, mtime) == "proposed"
|
||||
|
||||
|
||||
def _pref_key(
|
||||
|
||||
@@ -360,3 +360,90 @@ class TestQuoteTieBreak:
|
||||
with pytest.raises(ValueError, match="matches 2 paragraphs"):
|
||||
frlink.resolve("shared quote text appears here", store=s)
|
||||
s.close()
|
||||
|
||||
|
||||
# ── rule_kind ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _store_with_text(title: str, paragraphs: list[str]) -> tuple[Store, str]:
|
||||
s, key = _store_with_rule()
|
||||
s._con().execute("UPDATE items SET title = ? WHERE key = ?", (title, key)) # noqa: SLF001
|
||||
s._con().executemany( # noqa: SLF001
|
||||
"INSERT INTO fr_anchors (item_key, p_id, page, ordinal, text) VALUES (?,?,?,?,?)",
|
||||
[(key, i + 1, 100, i + 1, text) for i, text in enumerate(paragraphs)],
|
||||
)
|
||||
s._con().commit() # noqa: SLF001
|
||||
return s, key
|
||||
|
||||
|
||||
class TestRuleKind:
|
||||
def test_proposed_from_text_when_the_title_says_nothing(self) -> None:
|
||||
s, key = _store_with_text(
|
||||
"Medicare and Medicaid Programs; CY 2027 Payment Policies Under the PFS",
|
||||
[
|
||||
"In this proposed rule we propose to update the conversion factor.",
|
||||
"We are proposing to revise the telehealth list.",
|
||||
"Commenters on the CY 2026 final rule asked for more time.",
|
||||
],
|
||||
)
|
||||
assert frlink.rule_kind(s, key) == "proposed"
|
||||
|
||||
def test_final_from_text_when_the_title_says_nothing(self) -> None:
|
||||
s, key = _store_with_text(
|
||||
"Medicare and Medicaid Programs; CY 2025 Payment Policies Under the PFS",
|
||||
[
|
||||
"After considering comments, we are finalizing the policy as proposed.",
|
||||
"In this final rule we finalized the APCM codes.",
|
||||
"We proposed the change in the CY 2025 PFS proposed rule.",
|
||||
],
|
||||
)
|
||||
assert frlink.rule_kind(s, key) == "final"
|
||||
|
||||
def test_past_tense_we_proposed_is_not_a_proposed_signal(self) -> None:
|
||||
s, key = _store_with_text(
|
||||
"Medicare Program; CY 2021 Payment Policies Under the PFS",
|
||||
["We proposed this in July. We are finalizing it now in this final rule."],
|
||||
)
|
||||
assert frlink.rule_kind(s, key) == "final"
|
||||
|
||||
def test_silent_text_falls_back_to_the_title(self) -> None:
|
||||
s, key = _store_with_text(
|
||||
"Medicare Program; CY 2016 PFS Correction", ["Corrections to page 1."]
|
||||
)
|
||||
assert frlink.rule_kind(s, key) == "correction"
|
||||
s2, key2 = _store_with_text("Medicare Program; CY 2005 PFS Proposed Rule", [])
|
||||
assert frlink.rule_kind(s2, key2) == "proposed"
|
||||
|
||||
def test_undecidable_is_rule_never_final(self) -> None:
|
||||
s, key = _store_with_text("Medicare Program; Revisions to Payment Policies", [])
|
||||
assert frlink.rule_kind(s, key) == "rule"
|
||||
|
||||
def test_unknown_item_is_rule(self) -> None:
|
||||
s, _ = _store_with_rule()
|
||||
assert frlink.rule_kind(s, "NOPE0000") == "rule"
|
||||
|
||||
def test_kind_from_title(self) -> None:
|
||||
assert frlink.kind_from_title("CY 2025 PFS Proposed Rule") == "proposed"
|
||||
assert frlink.kind_from_title("CY 2025 PFS Final Rule") == "final"
|
||||
assert (
|
||||
frlink.kind_from_title("CY 2025 PFS Final Rule; Correction") == "correction"
|
||||
)
|
||||
assert frlink.kind_from_title("CY 2025 Payment Policies") == "rule"
|
||||
|
||||
def test_correction_title_wins_over_the_quoted_text(self) -> None:
|
||||
"""A correction notice quotes the rule it corrects ("this final
|
||||
rule …"), so its text is not evidence of its own kind."""
|
||||
s, key = _store_with_text(
|
||||
"Medicare Program; CY 2025 PFS Final Rule; Correction",
|
||||
[
|
||||
"On page 97864 of this final rule, we are finalizing … is corrected to read"
|
||||
],
|
||||
)
|
||||
assert frlink.rule_kind(s, key) == "correction"
|
||||
|
||||
def test_title_kwarg_skips_the_item_lookup(self) -> None:
|
||||
s, key = _store_with_text("x", ["In this proposed rule we propose a change."])
|
||||
assert (
|
||||
frlink.rule_kind(s, "NOPE0000", title="CY 2005 PFS Final Rule") == "final"
|
||||
)
|
||||
assert frlink.rule_kind(s, key, title="") == "proposed"
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
- item_key: XFGGRBDH
|
||||
p_id: 489
|
||||
expect_labels:
|
||||
- 'CY2025 PFS final .*¶489'
|
||||
# XFGGRBDH is the CY2025 *proposed* rule (91 FR, 2024-07-31); its title
|
||||
# carries no "Proposed", so the kind must come from the rule's text.
|
||||
- 'CY2025 PFS proposed .*¶489'
|
||||
expect_events:
|
||||
- code: "99441"
|
||||
kind: deleted|disappeared|cpt_deleted
|
||||
|
||||
@@ -148,6 +148,17 @@ class _FakeStore:
|
||||
"A temporary crosswalk between G2058 and new CPT code "
|
||||
"99439 (with a descriptor identical to G2058).",
|
||||
),
|
||||
# The proposed rule decides its kind from its own text —
|
||||
# bib.frlink.rule_kind counts "this proposed rule"/"we
|
||||
# propose" against "this final rule"/"we are finalizing".
|
||||
(
|
||||
"ZPROP2027",
|
||||
394,
|
||||
32388,
|
||||
1,
|
||||
"In this proposed rule we propose to keep the three-step "
|
||||
"telehealth review.",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -214,14 +225,44 @@ def _gd(
|
||||
|
||||
|
||||
class TestRuleLabel:
|
||||
def test_final(self):
|
||||
def test_title_without_a_kind_word_is_rule_not_final(self):
|
||||
"""Since CY2018 the CMS titles say neither "Proposed" nor
|
||||
"Final"; without a decided kind the label must not claim
|
||||
``final`` (a proposed rule presented as final is a false
|
||||
statement of policy)."""
|
||||
assert (
|
||||
rule_label(
|
||||
"Medicare Program; CY 2021 Payment Policies Under the PFS",
|
||||
"2020-12-28",
|
||||
1578,
|
||||
)
|
||||
== "CY2021 PFS final ¶1578"
|
||||
== "CY2021 PFS rule ¶1578"
|
||||
)
|
||||
|
||||
def test_final_from_title(self):
|
||||
assert (
|
||||
rule_label("Medicare Program; CY 2015 PFS Final Rule", "2014-11-13", 1251)
|
||||
== "CY2015 PFS final ¶1251"
|
||||
)
|
||||
|
||||
def test_kind_from_the_rule_text_overrides_the_title(self):
|
||||
"""The chat passes ``kind`` decided by ``bib.frlink.rule_kind``
|
||||
from the rule's own paragraphs — a title that says nothing
|
||||
still labels a proposed rule ``proposed``."""
|
||||
assert (
|
||||
rule_label(
|
||||
"Medicare and Medicaid Programs; CY 2027 Payment Policies",
|
||||
"2026-07-16",
|
||||
394,
|
||||
kind="proposed",
|
||||
)
|
||||
== "CY2027 PFS proposed ¶394"
|
||||
)
|
||||
|
||||
def test_correction_from_title(self):
|
||||
assert (
|
||||
rule_label("Medicare Program; CY 2016 PFS Correction", "2016-03-08", 12)
|
||||
== "CY2016 PFS correction ¶12"
|
||||
)
|
||||
|
||||
def test_proposed(self):
|
||||
@@ -242,6 +283,7 @@ class TestRuleLabel:
|
||||
1578,
|
||||
vol="85",
|
||||
page=84639,
|
||||
kind="final",
|
||||
)
|
||||
== "CY2021 PFS final 85 FR 84639 ¶1578"
|
||||
)
|
||||
@@ -253,6 +295,7 @@ class TestRuleLabel:
|
||||
"2020-12-28",
|
||||
1578,
|
||||
vol="85",
|
||||
kind="final",
|
||||
)
|
||||
== "CY2021 PFS final ¶1578"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user