9.2 KiB
FR Jump Links — Page/Paragraph Deep Links for Every FR Citation (P40)
Date: 2026-08-17. Milestone: P40. Requested as: a utility "able to grab
and place the paragraph link jump links from the web version of the
federal register to any of the pages or paragraphs in the rule,"
context-aware for ANY FR citation instance, with page ↔ paragraph
transmutation in both directions, Zotero child links, and :pincite:
integration. Design presented in-session and approved ("yes that does
it").
Verified ground truth (2026-08-17, CMS-1848-P / doc 2026-14327)
- The FR API's
body_html_urlserves the full rule body as one HTML file (4.2 MB for this 716-page rule) containing, in document order: 5,079 paragraph anchorsid="p-N", 716 page anchorsid="page-NNNNN", and — decisively — an explicitdata-page="NNNNN"attribute on each paragraph element, so page association needs no positional inference. - Deep links
#p-Nand#page-NNNNNwork against the canonicalhtml_urlreader page. - The API also returns
start_page/end_pageper document; bib rule items already carryfr_volume,fr_page,document_numberinextra_json, and ~30item_type='rule'items have federalregister.gov URLs. - Sample verified: page 44218's paragraphs (p-3599…) are the §414.1425 QP passage the CY2027 notebook cites as "91 FR 44218".
Decisions (user-selected)
- Consumers: all. One context-aware resolver used by notebooks,
the
:pincite:graph/docs, Zotero (as child link attachments), and a CLI. - Addressing: all modes. Quoted text, page + ordinal ("91 FR
44218 ¶3"), raw anchor id (
p-3600), and plain page cites; page ↔ paragraph transmutation both directions. - Persistence: in bib. Anchor maps stored in
bib.sqlite; offline, reproducible; refresh on demand. - Scope: all bib rules with FR URLs, backfilled.
Architecture
Home: new module src/bib/frlink.py — grabber, anchor store
access, resolver, transmutations, markdown helper. Three thin hooks
elsewhere: bib/pincite.py (FR locator grammar + URL resolution),
bib/sync.py (child link attachments), src/cli/bib.py (two
subcommands). Rationale: bib already owns the FR API client
(federalregister.py), the citation formatter, the pincite pipeline,
and the Zotero sync; frlink composes them. (Alternatives considered:
extending federalregister.py — blurs a stateless API client with a
stateful store; a new top-level package — overkill for citation
infrastructure.)
Data model (bib/schema.sql, auto-applied via CREATE TABLE IF NOT
EXISTS on Store init)
fr_anchors(id, item_key → items.key, p_id INTEGER, page INTEGER, ordinal INTEGER, text TEXT)— one row per paragraph anchor;ordinalis 1-based position on its printed page;textis the entity-decoded, tag-stripped paragraph text (for quote search). Unique on(item_key, p_id); indexed on(item_key, page).fr_anchor_docs(item_key PRIMARY KEY, document_number, html_url, body_html_url, start_page INTEGER, end_page INTEGER, fr_volume INTEGER, fetched_at, sha256, n_paragraphs, n_pages)— one row per grabbed rule; sha256 of the fetched body makes drift detectable.fr_links(id, item_key → items.key, p_id INTEGER NULL, page INTEGER, label TEXT, url TEXT, created_at)— placed links (the curated subset that gets carried into Zotero), unique on(item_key, url).
Grabber
grab(store, item_key, *, force=False) -> dict — resolves the item's
document_number (from extra_json, falling back to parsing the FR
URL), fetches body_html_url (one request; via the document API when
the body URL must be discovered), parses anchors with a pure function
parse_anchors(html) -> list[Anchor] (regex walk over id="p-N" /
data-page / id="page-N", tag-strip + html.unescape for text),
and replaces that item's fr_anchors partition + fr_anchor_docs
row. Backfill = iterate item_type='rule' items whose URL matches
federalregister.gov; skip already-grabbed unless --force.
Resolver (context-aware core)
resolve(ref, *, store, item_key=None) -> JumpLink where JumpLink
carries url, item_key, page, p_id, ordinal, snippet. Accepted
ref forms:
"91 FR 44218"→ page link{html_url}#page-44218. The rule is found by volume + page-range containment againstfr_anchor_docs(noitem_keyneeded); ambiguity (overlapping ranges) raises with candidates listed."91 FR 44218 ¶3"(alsopara 3/p.3spellings) → 3rd paragraph anchor on that page →#p-N."p-3600"(requiresitem_keyor an unambiguous single-rule context) → validated#p-3600.- A quoted passage ≥ 15 chars (anything not matching 1–3) →
normalized substring search over
fr_anchors.text(whitespace collapsed, entities already decoded); exactly-one match required, zero/many raise with diagnostics.
Transmutations: page_of(item_key, p_id) -> int and
paragraphs_of("91 FR 44218") -> list[Anchor] (with ordinals +
snippets) — page cites upgrade to paragraph cites and vice versa.
md_link(ref, *, text=None) -> str renders [91 FR 44218 ¶3](url)
for notebooks.
Placement
place(store, ref, *, label="") -> JumpLink — resolves, then upserts
an fr_links row. bib/sync.py gains _sync_fr_links beside
_sync_attachments: each fr_links row becomes a Zotero child
link attachment (linkMode=3 linked_url, contentType text/html,
itemData url + title = label), idempotent by existing child URL.
Zotero carries the links actually cited — never all ~5k anchors per
rule (explicit scope decision from the design review).
:pincite: integration: the locator grammar learns FR forms
(91 FR 44218, 91 FR 44218 ¶3, p-3600); Pincite gains a
jump_url(store) accessor used by the citation-graph/docs rendering;
unresolvable FR locators degrade to no-URL (never an exception in
graph rendering).
CLI
stack bib fr-grab [--all | --key KEY] [--force]— grab/backfill.stack bib fr-jump REF [--key KEY] [--md] [--place [--label L]]— resolve, print URL (or markdown), optionally place.
First consumer
The CY2027 NPRM notebook's FR pincites (at minimum the 91 FR 44218
QP-alignment cite and the CAA-2026 revival cites 44218-44219, 44286)
become live jump links via frlink.md_link, gated on the
nb_integration.py single-notebook run. Placed links for those cites
sync to Zotero as children of 5ITGVDJV.
Testing
parse_anchors: synthetic body-HTML fixture (pages, paragraphs, data-page, entities like§ , footnote-free edge cases); ordinal assignment; idempotent re-grab (partition replace).- Resolver: every ref form; ambiguity errors (multi-rule page range,
multi-hit quote); transmutations both ways;
md_linkoutput. - Placement: tmp bib + tmp zotero db (existing test_sync fixtures) — linkMode=3 child created once, idempotent on re-sync.
- Live smoke: parse the already-downloaded 4.2 MB CY2027 body HTML —
5,079 paragraphs / 716 pages expected;
resolve("91 FR 44218 ¶2")lands inside the §414.1425 passage.
Out of scope
- Anchors for non-rule item types (manuals, comments).
- Bulk-placing every anchor into Zotero.
- eCFR/edition permalinks (govinfo PDFs) — FR web version only.
- Auto-rewriting existing docstring pincites; the grammar accepts FR locators, adoption is incremental.
P40 build outcomes (2026-08-17)
Issues #634–#638, milestone P40 (6 commits, 5ea78cf..d7adf59, direct on main). All TDD; the pre-commit hook ran the bib-targeted suite green at each step (4,200+ tests).
- #634 (
5ea78cf):bib/frlink.pygrabber +fr_anchors/fr_anchor_docs/fr_linkstables +fr-grabCLI. Ground-truth correction: the CY2027 body carries 4,856 paragraph anchors (4,194<p>+ 662<li>), not the 5,079 stated above — that figure was agrep -cline-count artifact that also matchedid="page-lines. The parser handles both element kinds (close-tag aware) and captures 100% of real anchors; 431 of the 716 printed pages carry paragraph anchors (the rest are tables/addenda), and the resolver reports that distinction explicitly on ¶-of-page lookups. - #635 (
01e5d95):resolve()— page / page+¶-ordinal / raw anchor / quote refs, volume+page-range rule inference, ambiguity diagnostics;page_of/paragraphs_oftransmutations;md_link. Live:"91 FR 44218 ¶2"→#p-3601(the Participation-List ¶). - #636 (
eda1887): locator typesfr_para/fr_anchoradded (bare cites keep the backward-compatiblefr);Pincite.jump_url(degrades to"", never raises);CitationEdge.jump_urlpopulated bybuild_citation_graph. - #637 (
e5e5eb8):place()→fr_links;_sync_fr_linksin the Zotero push (linkMode=3 linked_url children, url+title itemData, idempotent by child URL);fr-jumpCLI with--md/--place. - #638 (
d7adf59): backfill grabbed 78 rules / 192,011 anchors, zero errors; CY2027 notebook cites are live links via a graceful-fallbackfr_md()helper (gatepass=1, resolution proven in-container); three placed links synced to Zotero as child link attachments on5ITGVDJV(incl. the §414.1425 QP-alignment paragraph pinned by quote →#p-3600). - Deviation: none of substance; the plan's ~30-rule estimate undercounted (bib holds 78 FR-linked rules; all grabbed).