docs(spec): FR jump links — page/paragraph deep links for FR citations (P40)
This commit is contained in:
156
docs/superpowers/specs/2026-08-17-fr-jump-links-design.md
Normal file
156
docs/superpowers/specs/2026-08-17-fr-jump-links-design.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# 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_url` serves the full rule body as one HTML
|
||||||
|
file (4.2 MB for this 716-page rule) containing, in document order:
|
||||||
|
5,079 paragraph anchors `id="p-N"`, 716 page anchors
|
||||||
|
`id="page-NNNNN"`, and — decisively — an explicit
|
||||||
|
`data-page="NNNNN"` attribute **on each paragraph element**, so
|
||||||
|
page association needs no positional inference.
|
||||||
|
- Deep links `#p-N` and `#page-NNNNN` work against the canonical
|
||||||
|
`html_url` reader page.
|
||||||
|
- The API also returns `start_page`/`end_page` per document; bib rule
|
||||||
|
items already carry `fr_volume`, `fr_page`, `document_number` in
|
||||||
|
`extra_json`, and ~30 `item_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;
|
||||||
|
`ordinal` is 1-based position on its printed page; `text` is 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:
|
||||||
|
|
||||||
|
1. `"91 FR 44218"` → page link `{html_url}#page-44218`. The rule is
|
||||||
|
found by volume + page-range containment against `fr_anchor_docs`
|
||||||
|
(no `item_key` needed); ambiguity (overlapping ranges) raises with
|
||||||
|
candidates listed.
|
||||||
|
2. `"91 FR 44218 ¶3"` (also `para 3` / `p.3` spellings) → 3rd
|
||||||
|
paragraph anchor on that page → `#p-N`.
|
||||||
|
3. `"p-3600"` (requires `item_key` or an unambiguous single-rule
|
||||||
|
context) → validated `#p-3600`.
|
||||||
|
4. 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_link` output.
|
||||||
|
- 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.
|
||||||
Reference in New Issue
Block a user