Files
stack/docs/superpowers/plans/2026-08-17-fr-jump-links-p40.md
kert 0f96fdba12
All checks were successful
CI / lint (push) Successful in 35s
CI / notebooks-smoke (push) Successful in 1m28s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 1m9s
Infra CI / zotero (push) Successful in 22s
Infra CI / docs (push) Successful in 1m13s
Infra CI / api (push) Successful in 1m14s
Infra CI / llm (push) Successful in 46s
Infra CI / mc (push) Successful in 14s
Deploy / report (push) Successful in 12s
CI / test (push) Successful in 11m49s
docs(spec): P40 build outcomes; plan checked off (refs #634-#638)
2026-08-17 14:45:36 -04:00

148 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P40: FR Jump Links Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** A context-aware `bib.frlink` utility that grabs each rule's federalregister.gov paragraph/page anchor map into bib and places deep jump links for any FR citation — notebooks, `:pincite:`, Zotero child links, CLI.
**Architecture:** One new module `src/bib/frlink.py` (grabber + resolver + placement) over three new `bib/schema.sql` tables (`fr_anchors`, `fr_anchor_docs`, `fr_links`), with thin hooks in `bib/pincite.py` (FR locator grammar + `jump_url`), `bib/sync.py` (`_sync_fr_links` → Zotero linkMode=3 children), and `src/cli/bib.py` (`fr-grab`, `fr-jump`). Rollout: backfill all FR-linked rules, then live-link the CY2027 notebook's citations and sync the placed links to Zotero.
**Tech Stack:** httpx (FR API + body HTML), sqlite3 via `bib.Store`, typer CLI, pytest; notebook gate via `nb_integration.py` in the prod notebooks container.
**Spec:** `docs/superpowers/specs/2026-08-17-fr-jump-links-design.md` (committed; design user-approved in-session). Tracker (real links):
- Milestone [P40](https://git.fhirworx.io/homelab/stack/milestone/41)
- [#634 — schema + grabber + fr-grab/backfill](https://git.fhirworx.io/homelab/stack/issues/634)
- [#635 — context-aware resolver + transmutations](https://git.fhirworx.io/homelab/stack/issues/635)
- [#636 — :pincite: FR locators + jump_url](https://git.fhirworx.io/homelab/stack/issues/636)
- [#637 — place() + Zotero child link attachments](https://git.fhirworx.io/homelab/stack/issues/637)
- [#638 — rollout: backfill + notebook + Zotero](https://git.fhirworx.io/homelab/stack/issues/638)
## Global Constraints
- Conventional commits, `(closes #N)`/`(refs #N)`; **no Co-Authored-By trailer**.
- TDD per task; `uv run pytest tests/bib/test_frlink.py -q` (new file) plus touched existing test files must be green before each commit.
- `parse_anchors` and all resolvers are **pure functions over stored data** — network only in `grab()`/backfill.
- Backfill is polite to the FR API: sequential fetches, small sleep, skip already-grabbed (no `--force` in the rollout run).
- Zotero writes only through the existing sync path (container held); never bulk-place anchors.
- Verified anchors (CY2027, doc 2026-14327): 5,079 `p-N`, 716 `page-N`, `data-page` on every paragraph; body HTML cached at the P40 scratchpad (`fr_body.html`) for offline smoke tests.
---
### Task 1: Schema + `parse_anchors` + `grab` + `fr-grab` CLI ([#634](https://git.fhirworx.io/homelab/stack/issues/634))
**Files:**
- Modify: `src/bib/schema.sql` (three tables + indexes), `src/cli/bib.py` (fr-grab)
- Create: `src/bib/frlink.py`
- Test: `tests/bib/test_frlink.py` (new)
**Interfaces:**
- Produces:
- `Anchor` dataclass: `p_id: int, page: int, ordinal: int, text: str`
- `parse_anchors(html: str) -> list[Anchor]` — pure; walks `id="p-N"` with `data-page`, assigns 1-based ordinal per page, tag-strips + `html.unescape`s + whitespace-collapses text
- `grab(store, item_key, *, force=False, fetch=None) -> dict` — resolves `document_number` (item `extra_json`, fallback URL parse), fetches the doc API for `body_html_url`/`start_page`/`end_page`/`volume` unless already in `fr_anchor_docs`, replaces the item's `fr_anchors` partition + `fr_anchor_docs` row; returns counts. `fetch` injectable for tests.
- `backfill(store, *, force=False, sleep=1.0) -> list[dict]` over `item_type='rule'` items with federalregister.gov URLs.
- Schema: as specced — `fr_anchors` UNIQUE(item_key, p_id) + idx (item_key, page); `fr_anchor_docs` PK item_key; `fr_links` UNIQUE(item_key, url).
- [x] **Step 1 (failing tests):** `tests/bib/test_frlink.py` with a synthetic body-HTML fixture:
```python
FIXTURE_HTML = """
<html><body>
<div id="page-100"></div>
<p id="p-1" data-page="100">First para on 100 with &sect;&thinsp;414.1425 text.</p>
<p id="p-2" data-page="100">Second para <em>with markup</em> on 100.</p>
<div id="page-101"></div>
<p id="p-3" data-page="101">Only para on 101.</p>
</body></html>
"""
def test_parse_anchors_pages_ordinals_text():
a = frlink.parse_anchors(FIXTURE_HTML)
assert [(x.p_id, x.page, x.ordinal) for x in a] == [(1, 100, 1), (2, 100, 2), (3, 101, 1)]
assert "§ 414.1425" in a[0].text.replace("", " ") or "§" in a[0].text
assert a[1].text == "Second para with markup on 100."
def test_grab_persists_and_replaces(tmp_path):
# Store with a rule item carrying extra_json document_number/volume;
# grab(fetch=lambda ...) injecting FIXTURE_HTML + fake doc meta;
# assert fr_anchors rows == 3, fr_anchor_docs row present;
# re-grab with a 2-anchor fixture → rows == 2 (partition replaced).
```
(write both fully, using `Store(":memory:")` + a `Rule` item with `extra_json` fields matching the real shape: `{"document_number": "2026-14327", "fr_volume": "91", "fr_page": "43842"}`)
- [x] **Step 2:** Run: `uv run pytest tests/bib/test_frlink.py -q` → FAIL (module missing).
- [x] **Step 3:** Implement `src/bib/frlink.py` (`_P_RE = re.compile(r'id="p-(\d+)"[^>]*data-page="(\d+)"')`; text = substring to the next `</p>`, tag-strip + unescape + `" ".join(split())`), schema additions, and the `fr-grab` typer command calling `grab`/`backfill`.
- [x] **Step 4:** `uv run pytest tests/bib/test_frlink.py tests/bib/test_store.py -q` → PASS (store tests prove schema addition breaks nothing).
- [x] **Step 5:** Offline real-file smoke: run `parse_anchors` on the cached CY2027 `fr_body.html` → assert 5079 paragraphs, 716 distinct pages (one-off command, not a committed test — the 4.2 MB file stays out of the repo).
- [x] **Step 6:** Commit: `feat(bib): fr_anchors schema + FR body-HTML grabber + fr-grab CLI (closes #634)`
### Task 2: Resolver + transmutations + `md_link` ([#635](https://git.fhirworx.io/homelab/stack/issues/635))
**Files:**
- Modify: `src/bib/frlink.py`
- Test: `tests/bib/test_frlink.py`
**Interfaces:**
- Produces:
- `JumpLink` dataclass: `url, item_key, page, p_id (None for page links), ordinal (None), snippet ("")`
- `resolve(ref, *, store, item_key="") -> JumpLink` — ref grammar precedence: raw anchor `^p-\d+$` → FR cite `^(\d+)\s+FR\s+(\d+)(?:[,\s]+(?:¶|para\.?|p\.)\s*(\d+))?$` (page, optional ordinal) → otherwise quote (≥15 chars after normalization; else ValueError). FR cites resolve the rule by `fr_volume` + `start_page ≤ page ≤ end_page` over `fr_anchor_docs`; 0 or >1 candidates → ValueError naming them (unless `item_key` given).
- `page_of(store, item_key, p_id) -> int`, `paragraphs_of(ref, *, store) -> list[Anchor]`
- `md_link(ref, *, store, item_key="", text="") -> str`
- URL forms: `{html_url}#page-{page}` / `{html_url}#p-{p_id}`
- [x] **Step 1 (failing tests):** extend the fixture store helper to populate `fr_anchor_docs` (html_url `https://example.test/doc`, volume 91, pages 100101) + the 3 fixture anchors; add tests: page ref, page+ordinal (`"91 FR 100 ¶2"``#p-2`), raw `p-3`, quote (`"Only para on 101"``#p-3`), quote-multi-hit raises, unknown page raises, `page_of`/`paragraphs_of` round-trip, `md_link` default text equals the ref.
- [x] **Step 2:** Run → new tests FAIL.
- [x] **Step 3:** Implement; quote normalization shared with `parse_anchors` storage form.
- [x] **Step 4:** `uv run pytest tests/bib/test_frlink.py -q` → PASS.
- [x] **Step 5:** Commit: `feat(bib): context-aware FR jump resolver — page/ordinal/anchor/quote + transmutations (closes #635)`
### Task 3: `:pincite:` FR locators + `jump_url` ([#636](https://git.fhirworx.io/homelab/stack/issues/636))
**Files:**
- Modify: `src/bib/pincite.py` (`_classify_locator` + `Pincite.jump_url` + graph edge URL)
- Test: `tests/bib/test_pincite.py` (extend)
**Interfaces:**
- Consumes: `frlink.resolve` (lazy import inside `jump_url` to avoid import cycles).
- Produces: locator_type `"fr_page"` (`91 FR 44218`), `"fr_para"` (`91 FR 44218 ¶3`), `"fr_anchor"` (`p-3600`); `Pincite.jump_url(store) -> str` ("" when unresolvable — never raises); graph edges carry `metadata["jump_url"]` when non-empty.
- [x] **Step 1 (failing tests):** classification of the three forms (existing forms `p.14`/`§3.2` keep their current types — assert unchanged); `jump_url` resolves against the Task-2 fixture store; absent anchor map → `""`.
- [x] **Step 2:** Run → FAIL. **Step 3:** Implement. **Step 4:** `uv run pytest tests/bib/test_pincite.py tests/bib/test_frlink.py -q` → PASS.
- [x] **Step 5:** Commit: `feat(bib): :pincite: FR locators resolve to jump URLs (closes #636)`
### Task 4: `place()` + Zotero linkMode=3 children + `fr-jump` CLI ([#637](https://git.fhirworx.io/homelab/stack/issues/637))
**Files:**
- Modify: `src/bib/frlink.py` (`place`), `src/bib/sync.py` (`_sync_fr_links`, called from the push loop next to `_sync_attachments`), `src/cli/bib.py` (`fr-jump`)
- Test: `tests/bib/test_frlink.py`, `tests/bib/test_sync.py` (extend, `_make_zotero_db` fixture)
**Interfaces:**
- Produces: `place(store, ref, *, item_key="", label="") -> JumpLink` (upsert `fr_links`; default label = `"¶ p-N — {ref}"` or the page ref); `_sync_fr_links(db, store, bib_item, zot_parent_id) -> int` creating child attachments via `db.add_attachment(parent, link_mode=3, content_type="text/html")` + `db.set_fields(att_id, {"url": url, "title": label})`, idempotent by existing children's `url` itemData; `fr-jump REF [--key K] [--md] [--place] [--label L]` CLI.
- [x] **Step 1 (failing tests):** `place` writes one `fr_links` row, double-place no-ops; sync test: bib item + placed link → push creates exactly one linkMode=3 child with url+title itemData; second push creates none.
- [x] **Step 2:** Run → FAIL. **Step 3:** Implement. **Step 4:** `uv run pytest tests/bib/test_frlink.py tests/bib/test_sync.py -q` → PASS.
- [x] **Step 5:** Commit: `feat(bib): place FR jump links as Zotero child link attachments + fr-jump CLI (closes #637)`
### Task 5: Rollout ([#638](https://git.fhirworx.io/homelab/stack/issues/638))
**Files:**
- Modify: `notebooks/cy2027_pfs_proposed_rule.py` (live-link FR cites). Data: `bib.sqlite`, `zotero.sqlite`.
- [x] **Step 1:** `uv run stack bib fr-grab --all` — backfill; report per-rule paragraph/page counts; expect ~30 rules, CY2027 = 5079/716. Failures on odd old rules: log + continue, note on the issue.
- [x] **Step 2:** Notebook: in the APM narrative cell and Section-5 sources, replace the dead `91 FR 44218-44219, 44286` style cites with `frlink.md_link(...)` calls (import in the notebook's setup cell; reads bib.sqlite via `conf.connect.bib()` — available in the notebooks container through the mounted `data/`). In-cell fallback: if the anchor map is missing, degrade to plain text (notebook must not break offline).
- [x] **Step 3:** Gate: single-notebook `nb_integration.py` run in the prod container → `pass=1`.
- [x] **Step 4:** Place the cited paragraphs (`91 FR 44218` QP-alignment ¶, the 414.1450(b)(1) revival cite) via `stack bib fr-jump ... --place`, then `uv run stack bib sync-zotero --tag sup:2027_PFS_NPRM`; verify linkMode=3 children on `5ITGVDJV` in zotero.sqlite.
- [x] **Step 5:** Commit notebook: `feat(notebooks): live FR jump links in CY2027 NPRM citations (closes #638)`
### Task 6: Close the loop
- [x] **Step 1:** Push; CI green on HEAD (poll with `skipped` treated as terminal — see gitea-tracker-ops memory).
- [x] **Step 2:** Issues #634#638 closed (auto-close footers) with verification comments where data-only; close [milestone P40](https://git.fhirworx.io/homelab/stack/milestone/41).
- [x] **Step 3:** Append P40 build outcomes to the spec; commit docs.
- [x] **Step 4:** Memory: new `fr_jump_links.md` (anchor-map mechanics, `data-page` fact, resolver grammar, don't-bulk-place rule).
## Self-Review
- Coverage: #634→T1, #635→T2, #636→T3, #637→T4, #638→T5, closure→T6; spec sections all mapped (grammar, transmutations, placement, CLI, rollout).
- Placeholders: none — regexes, fixtures, URL forms, and CLI shapes are spelled out; Step-1 test stubs state exact assertions.
- Type consistency: `Anchor(p_id, page, ordinal, text)` used by T1/T2/T3 alike; `JumpLink` fields consistent across resolve/place/md_link; `fr_links` UNIQUE(item_key,url) matches place-idempotency tests.