feat(bib): :pincite: FR locators (fr_para/fr_anchor) resolve to jump URLs (closes #636)

This commit is contained in:
kert
2026-08-17 14:32:36 -04:00
parent 01e5d9588b
commit eda1887628
2 changed files with 115 additions and 1 deletions

View File

@@ -67,6 +67,22 @@ class Pincite(BaseModel):
locator_type: str = "" locator_type: str = ""
note: str = "" note: str = ""
def jump_url(self, store: Store) -> str:
"""Deep federalregister.gov link for FR-typed locators (#636).
Returns ``""`` for non-FR locators and whenever resolution
fails (no anchor map grabbed, page out of range, …) — graph
and docs rendering must degrade to no-URL, never raise.
"""
if self.locator_type not in ("fr", "fr_para", "fr_anchor"):
return ""
try:
from bib import frlink
return frlink.resolve(self.locator, store=store, item_key=self.item_key).url
except Exception: # noqa: BLE001 — degrade to no-URL by contract
return ""
@dataclass @dataclass
class CitationNode: class CitationNode:
@@ -87,6 +103,7 @@ class CitationEdge:
locator: str = "" locator: str = ""
locator_type: str = "" locator_type: str = ""
note: str = "" note: str = ""
jump_url: str = ""
@dataclass @dataclass
@@ -153,11 +170,20 @@ def _classify_locator(locator: str) -> str:
if not locator: if not locator:
return "" return ""
loc = locator.strip() loc = locator.strip()
if re.match(r"^p-\d+$", loc):
# A raw federalregister.gov paragraph anchor (#636) — checked
# before "p." so it never reads as a page locator.
return "fr_anchor"
if loc.lower().startswith(("p.", "pp.")): if loc.lower().startswith(("p.", "pp.")):
return "page" return "page"
if "CFR" in loc or "cfr" in loc: if "CFR" in loc or "cfr" in loc:
return "cfr" return "cfr"
if re.match(r"\d+\s+FR\s+\d+", loc, re.IGNORECASE): m = re.match(r"\d+\s+FR\s+\d+", loc, re.IGNORECASE)
if m:
# "91 FR 44218 ¶3" (or "para 3"/"p.3") pins a paragraph on the
# cited page; a bare cite stays the backward-compatible "fr".
if re.search(r"(?:¶|\bpara\.?\s*|\bp\.\s*)\d+", loc[m.end() :]):
return "fr_para"
return "fr" return "fr"
if loc.lower().startswith(("ch.", "ch ")): if loc.lower().startswith(("ch.", "ch ")):
return "chapter" return "chapter"
@@ -435,6 +461,7 @@ def build_citation_graph(store: Store) -> CitationGraph:
locator=p.locator, locator=p.locator,
locator_type=p.locator_type, locator_type=p.locator_type,
note=p.note, note=p.note,
jump_url=p.jump_url(store),
) )
) )

View File

@@ -495,3 +495,90 @@ class TestUpsertPincitesTagFailure:
[Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")], [Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")],
) )
assert count == 1 assert count == 1
# ── FR locators + jump_url (#636) ───────────────────────────────────
class TestFrLocators:
def test_plain_fr_cite_keeps_fr_type(self) -> None:
assert _classify_locator("91 FR 44218") == "fr"
def test_fr_para_locator(self) -> None:
assert _classify_locator("91 FR 44218 ¶3") == "fr_para"
assert _classify_locator("91 FR 44218, para 3") == "fr_para"
assert _classify_locator("91 FR 44218 p.3") == "fr_para"
def test_fr_anchor_locator(self) -> None:
assert _classify_locator("p-3600") == "fr_anchor"
def test_existing_types_unchanged(self) -> None:
assert _classify_locator("p.14") == "page"
assert _classify_locator("§3.2") == "section"
assert _classify_locator("42 CFR 425.400") == "cfr"
class TestJumpUrl:
def _frlink_store(self):
from bib import frlink
from tests.bib.test_frlink import DOC_META, FIXTURE_HTML, _store_with_rule
s, key = _store_with_rule()
frlink.grab(s, key, fetch=lambda _doc: (DOC_META, FIXTURE_HTML))
return s, key
def test_jump_url_resolves_fr_para(self) -> None:
s, key = self._frlink_store()
p = Pincite(
fn_path="mod.fn",
item_key=key,
locator="91 FR 100 ¶2",
locator_type="fr_para",
)
assert p.jump_url(s) == "https://example.test/doc#p-2"
s.close()
def test_jump_url_resolves_plain_fr(self) -> None:
s, key = self._frlink_store()
p = Pincite(
fn_path="mod.fn", item_key=key, locator="91 FR 100", locator_type="fr"
)
assert p.jump_url(s) == "https://example.test/doc#page-100"
s.close()
def test_jump_url_empty_when_unresolvable(self) -> None:
s, key = self._frlink_store()
# non-FR locator → no URL; missing anchor map → no URL, no raise
p = Pincite(
fn_path="mod.fn", item_key=key, locator="§3.2", locator_type="section"
)
assert p.jump_url(s) == ""
p2 = Pincite(
fn_path="mod.fn", item_key=key, locator="91 FR 999", locator_type="fr"
)
assert p2.jump_url(s) == ""
s.close()
class TestGraphJumpUrl:
def test_edges_carry_jump_url(self) -> None:
from bib import frlink
from tests.bib.test_frlink import DOC_META, FIXTURE_HTML, _store_with_rule
s, key = _store_with_rule()
frlink.grab(s, key, fetch=lambda _doc: (DOC_META, FIXTURE_HTML))
upsert_pincites(
s,
[
Pincite(
fn_path="mod.fn",
item_key=key,
locator="91 FR 100 ¶2",
locator_type="fr_para",
)
],
)
g = build_citation_graph(s)
edge = next(e for e in g.edges if e.source == "mod.fn")
assert edge.jump_url == "https://example.test/doc#p-2"
s.close()