fix(llm,pfs): I5/M1/M2 — probe the model pool once per classifier, strip FR citations before lineage pattern matching, drop unused extract_text param

I5: `closed_vocab_classifier` called `pool.check(cfg.instruct_model)` on
every `classify()` call — a host-liveness probe, not per-request work.
A code with hundreds of element lines re-probed the pool hundreds of
times. Move the check to the closure's construction.

M1: `fr_events` matched event patterns (crosswalk, replaces, …) against
the raw FR paragraph text, so a citation like "91 FR 99490" inside a
"crosswalk ... 91 FR 99490" sentence read as a real crosswalk event for
99490 — the citation's page number is not a mention of the code. Strip
FR citations with the now-public `pfs.families.FR_CITE_RE` before both
the presence check and every event pattern.

M2: `extract_text`'s `classify` parameter was unused; no call site
passed it. Drop it.

Claude-Session: https://claude.ai/code/session_01Aum3pEMAM3yQVdFSdVe6Gc
This commit is contained in:
kert
2026-09-09 14:10:55 -04:00
parent 9e374e8c29
commit 7baa789bc0
5 changed files with 41 additions and 6 deletions

View File

@@ -56,11 +56,15 @@ def _default_post(url: str, json: dict) -> dict:
def closed_vocab_classifier( def closed_vocab_classifier(
cfg: Any, pool: Any, *, post: Callable[..., dict] | None = None cfg: Any, pool: Any, *, post: Callable[..., dict] | None = None
) -> Classifier: ) -> Classifier:
"""Return ``classify(text, choices) -> slug | None`` bound to *pool*.""" """Return ``classify(text, choices) -> slug | None`` bound to *pool*.
``pool.check`` runs once here, not per call (I5) — it's a host-liveness
probe, not per-request work, and a code with hundreds of element lines
used to re-probe the pool hundreds of times."""
send = post or _default_post send = post or _default_post
pool.check(cfg.instruct_model)
def classify(text: str, choices: Sequence[str]) -> str | None: def classify(text: str, choices: Sequence[str]) -> str | None:
pool.check(cfg.instruct_model)
with pool.acquire_generation() as host: with pool.acquire_generation() as host:
model = pick_model(cfg, pool, host) model = pick_model(cfg, pool, host)
data = send( data = send(

View File

@@ -80,9 +80,7 @@ def extract_run(
return Extraction(run.code, tuple(ordered), tuple(reviews)) return Extraction(run.code, tuple(ordered), tuple(reviews))
def extract_text( def extract_text(code: str, text: str, *, year: int, source: str) -> Extraction:
code: str, text: str, *, year: int, source: str, classify: Classifier | None = None
) -> Extraction:
rows = [ rows = [
ElementRow(code, year, e.type.value, e.value, e.detail, text, "", 0, 0, source) ElementRow(code, year, e.type.value, e.value, e.detail, text, "", 0, 0, source)
for e in parse_descriptor(text) for e in parse_descriptor(text)

View File

@@ -19,7 +19,7 @@ from typing import Any
from pfs.codetables import EventRow from pfs.codetables import EventRow
from pfs.descriptors import rule_year_of from pfs.descriptors import rule_year_of
from pfs.families import find_codes from pfs.families import FR_CITE_RE, find_codes
RVU_KINDS = ( RVU_KINDS = (
"appeared", "appeared",
@@ -199,6 +199,11 @@ def fr_events(store: Any, code: str) -> list[EventRow]:
out: list[EventRow] = [] out: list[EventRow] = []
for r in rows: for r in rows:
text = _expand_ranges(r["text"]) text = _expand_ranges(r["text"])
# An FR page citation's number ("91 FR 99490") is not a mention of
# the code — strip it before both the presence check and every
# event pattern below (M1: a "crosswalk ... 91 FR 99490" sentence
# used to read as a crosswalk event for 99490).
text = FR_CITE_RE.sub(" ", text)
if code not in text.upper(): if code not in text.upper():
continue continue
year = rule_year_of(r["title"], r["date_published"]) year = rule_year_of(r["title"], r["date_published"])

View File

@@ -93,3 +93,14 @@ class TestClassifier:
post=lambda url, json: {"message": {"content": "maybe consent?"}}, post=lambda url, json: {"message": {"content": "maybe consent?"}},
) )
assert classify("x", ["consent"]) is None assert classify("x", ["consent"]) is None
def test_pool_check_runs_once_not_per_call(self):
# I5: pool.check is a host-liveness probe, done once when the
# classifier closure is built — not per classify() call.
pool = _Pool()
classify = closed_vocab_classifier(
_Cfg(), pool, post=lambda url, json: {"message": {"content": "consent"}}
)
classify("a", ["consent"])
classify("b", ["consent"])
assert pool.checked == ["qwen2.5:14b"]

View File

@@ -189,6 +189,23 @@ class TestFrEvents:
kinds = {e.kind for e in fr_events(store, "99441")} kinds = {e.kind for e in fr_events(store, "99441")}
assert {"deleted", "bundled", "telehealth_list"} <= kinds assert {"deleted", "bundled", "telehealth_list"} <= kinds
def test_fr_citation_page_number_is_not_a_crosswalk_mention(self, store):
# M1: an FR page citation ("91 FR 99490") is not a real mention of
# 99490 — it must be stripped before pattern matching, or a
# "crosswalk ... 91 FR 99490" sentence reads as a crosswalk event.
store.con.execute(
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)",
(
"DE2VH9PD",
1500,
67999,
1500,
"We note the crosswalk methodology discussed at 91 FR 99490 for related codes.",
),
)
ev = fr_events(store, "99490")
assert not any(e.kind == "crosswalk" for e in ev)
def test_deleted_via_range_prefilter_middle_member(self, store): def test_deleted_via_range_prefilter_middle_member(self, store):
# 90868 is a middle member of a "90867-90869" range and appears # 90868 is a middle member of a "90867-90869" range and appears
# nowhere else in the paragraph literally — the SQL prefilter must # nowhere else in the paragraph literally — the SQL prefilter must