docs(spec): eCFR jump links — bidirectional CFR cite/URL (P41)

This commit is contained in:
kert
2026-08-17 14:50:21 -04:00
parent 0f96fdba12
commit 9eb1e52266

View File

@@ -0,0 +1,126 @@
# eCFR Jump Links — Bidirectional CFR Cite ↔ URL (P41)
Date: 2026-08-17. Milestone: P41. Requested as: "the same [as P40's FR
jump links] for eCFR — so a CFR cite goes back and forth."
## Verified ground truth (2026-08-17)
- `https://www.ecfr.gov/current/title-42/section-425.400` (short form)
**302-redirects server-side** to the canonical hierarchy URL
(`/current/title-42/chapter-IV/subchapter-B/part-425/subpart-E/section-425.400`)
— cite → URL needs no structure lookup; fragments survive redirects
client-side.
- Paragraph anchors on section pages are **semantic**:
`id="p-425.400(c)(1)(iv)"` — the fragment IS the cite's paragraph
path (316 such anchors on § 425.400). Both directions are therefore
deterministic string transforms.
- The eCFR versioner API is open (`/api/versioner/v1/titles.json`
responds), but the plain `/cite/` permalink endpoint 406s for
non-browser clients — irrelevant, the short form covers it.
- bib holds **699 `item_type='regulation'` items with eCFR URLs** —
both part-level (`…/title-42/part-414`) and section-level
(`…/part-405/section-405.534`) — the Title 42/45 Regulations
collections. These are the placement targets and the validation
index.
## Decisions (inherited from P40's user-selected pattern)
Consumers: all (notebooks, `:pincite:`, Zotero child links, CLI).
Addressing: cite (with/without paragraph path), URL (inverse), both
directions. Persistence: no anchor grab needed — the transform is
deterministic; the bib regulation index provides context/validation.
Placement discipline: only `place()`d links reach Zotero.
## Architecture
**`src/bib/cfrlink.py`** — parse, transmute, context, place. Hooks:
`bib/pincite.py` (`jump_url` covers the existing `cfr` locator type),
notebook helper `cfr_md()`, CLI `stack bib cfr-jump`. Zotero placement
reuses the P40 `fr_links` table + `_sync_fr_links` path unchanged
(rows carry `page=0` and `p_id=NULL` for non-FR links — the sync only
reads `label`/`url`; documented here rather than forking a parallel
table).
### Data model
`CfrCite` frozen dataclass: `title: int, part: str, section: str
("425.400", empty for part-level cites), paras: tuple[str, ...]
(("c","1","iv")), date: str ("" = current)`.
### Parse (`parse_cite(ref) -> CfrCite`)
Accepted spellings (case-insensitive, `C.F.R.` dots optional, `§`
optional, whitespace collapsed):
- `42 CFR 425.400(c)(1)(iv)` / `42 C.F.R. § 425.400(c)(1)(iv)`
- `42 CFR § 414.1425` / `42 CFR 414.1450(b)(1)`
- `42 CFR Part 425` / `42 CFR part 425 subpart E` (subpart ignored for
URL purposes — the short form resolves placement)
Title is required (bare `§ 425.400` raises naming the reason — the
title selects the eCFR tree). Paragraph path parsed from consecutive
parenthesized tokens.
### Transmute
- `url(cite) -> str`:
section: `https://www.ecfr.gov/current/title-{t}/section-{s}` +
(`#p-{s}({a})({b})…` when paras present);
part: `https://www.ecfr.gov/current/title-{t}/part-{p}`;
date-pinned: `/on/{date}/` replaces `/current/` when `cite.date`.
- `cite_of(url) -> str`: inverse over every eCFR URL form — short,
canonical full-hierarchy, `/on/{date}/`, with or without `#p-…`
fragment. Emits the canonical spelling `42 CFR 425.400(c)(1)(iv)`
(part-level: `42 CFR Part 425`).
- **Round-trip law**: `cite_of(url(parse_cite(x)))` == canonical
spelling of `x`, and `url(parse_cite(cite_of(u)))` reproduces `u`'s
short form — tested explicitly.
### Bib context + placement
- `item_for(cite, store) -> str | ""`: match against regulation
items' eCFR URLs — exact section item first, then the part item.
Powers placement targets and lightweight validation ("is this cited
reg in the library?"); a cite with no library item still transmutes
(URL construction never requires the store).
- `place(store, ref, *, label="") -> JumpLink`: resolve + `item_for`
(raises when no regulation item matches — placed links need a
parent), upsert into `fr_links` (page=0), synced by the existing
`_sync_fr_links` as linkMode=3 children.
- `md_link(ref, *, text="")` — store-free (deterministic URL).
- CLI: `stack bib cfr-jump REF [--md] [--place [--label L]]` and
`stack bib cfr-jump --url URL` for the reverse direction (prints the
cite).
### `:pincite:`
`Pincite.jump_url` gains the `cfr` branch: locators like
`42 CFR 425.400(c)` resolve through `cfrlink` (no store data needed);
non-title locators (`§ 3.2`-style, classified `section`) stay
URL-less. Degradation contract unchanged (`""`, never raise).
### Notebook
`cfr_md()` helper beside `fr_md()` (same graceful fallback shape, but
store-free); the CY2027 notebook's `42 CFR 414.1450(b)(1)` (APM
revival), `§ 414.1425(c)(5)(ii)` (QP alignment — spelled with title),
and the PQM section's `42 CFR § 425.400(c)` cites become live eCFR
links. Gate: `nb_integration.py` single-notebook run.
## Testing
- Parse: every accepted spelling + reject-without-title + part form.
- Transmute: URL forms incl. date pin and fragments; `cite_of` over
short/canonical/dated/fragmented URLs; both round-trip laws.
- Context: `item_for` section-over-part preference against a fixture
store; `place` on a regulation item; sync reuses the P40 linkMode=3
tests' path (one new test: CFR-placed row syncs identically).
- Live smoke: `url(parse_cite("42 CFR 425.400(c)(1)(iv)"))` fetches
200 with the fragment's anchor present in the body.
## Out of scope
- eCFR structure/versioner grabs (no data needed for the transform;
revisit only if full-hierarchy canonical URLs or historical
snapshots become requirements).
- Quote → paragraph addressing for CFR text (would need section text
ingestion; FR quotes already cover the rulemaking corpus).
- Auto-creating regulation items for cites not in the library.