Skip to content

Book-citation grounding + Open Library enrichment mechanism (PRD-0009, ADR-0018/0019)#152

Closed
tieguy wants to merge 10 commits into
mainfrom
claude/openlibrary-resolve
Closed

Book-citation grounding + Open Library enrichment mechanism (PRD-0009, ADR-0018/0019)#152
tieguy wants to merge 10 commits into
mainfrom
claude/openlibrary-resolve

Conversation

@tieguy

@tieguy tieguy commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements PRD-0009 end to end, governed by two new ADRs written first:

  • ADR-0018 — the Open Library / Internet Archive read contract: identifier gating, the side-effect-free Books API lookup (never the /isbn/{isbn} import-on-miss path), exact-vs-similar scan availability via the Read API, and search-inside grounding with a provenance-scoped short-body bypass.
  • ADR-0019 — the Open Library apply contract: two write lanes (REST PUT, usergroup-gated server-side, with pre-mutation 403 fallback to a fail-closed edit-form adapter), per-session lane cache, client-side refuse-on-drift on record revision, post-apply read-back — and the enablement gate that keeps the whole write lane disabled and unwired in this PR.

What lands, layer by layer:

  1. Identifier extraction — validated BookIdentifier (ISBN-10/13 with checksums, OCLC, LC-normalized LCCN, edition-only OLID) parsed structurally from cite-template data-mw; BlockRef gains book_sources.
  2. Resolve (Layer 1)verify-page resolves each book ref through side-effect-free Open Library lookups: Books API catalog record, then Read API exact-vs-similar scan availability. Clean miss → NotFound; transport/shape failure → LookupFailed; a lookup failure never fails the page.
  3. Ground (Layer 2) — a resolved book with an exact-edition scan is grounded against Internet Archive search-inside snippets: cited-page-first with whole-book fallback, snippet body feeding the existing verdict panel, page-anchored deep links in provenance, and the honest split — indexed-but-no-matches → not_supported, no usable body → SourceUnavailable. Never verifies against a merely similar edition.
  4. Enrich (Layer 3, mechanism only) — deterministic ISBN-10↔13 completion candidates (conversions only, never verbatim relay), proposal building pinned to record key + revision, and the full apply machinery per ADR-0019 (S3-key login, REST lane, form-adapter that fails closed on any contract drift, read-back verification). ENRICHMENT_WRITE_LANE_ENABLED = false with a tripwire test; nothing calls the apply path.
  5. Reporting — a Books section in the page report (every resolution, scan status, enrichment candidates as read-only propose= lines) plus book_scan provenance on findings.
  6. Manual smokescripts/openlibrary-contract-smoke.sh exercises the live read contract (Books API → Read API → metadata → search-inside) with field assertions matching the parsers; it must run from an operator machine (Open Library blocks datacenter egress).

Branch is merged with current main; the book lane and the bare-URL repair lane (#109) coexist (BlockRef carries both book_sources and is_bare_url_ref; CitationFinding carries both book_scan and is_bare_url_ref).

Type

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Build, CI, or release
  • Security-sensitive change

Validation Required

cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features   # pedantic clean
cargo test --workspace                                  # 0 failed
cargo check -p sp42-app --target wasm32-unknown-unknown
./scripts/check-layering.sh
./scripts/check-design-system.sh
./scripts/check-doc-consistency.sh
./scripts/check-links.sh
./scripts/generate-architecture-map.sh --check

Manual (operator machine, not CI): scripts/openlibrary-contract-smoke.sh against the live services — the sandbox cannot reach openlibrary.org/archive.org.

Risk

  • Touches auth/session/cookies/CSRF
  • Touches deployment/runtime configuration
  • Touches desktop packaging
  • Touches scoring or Wikimedia action behavior
  • Needs an ADR

Auth surface: the ADR-0019 apply machinery includes Open Library S3-key login and session-cookie handling — but it is dead code behind ENRICHMENT_WRITE_LANE_ENABLED = false (tripwire-tested), reachable only from its own unit tests. No Wikimedia action behavior changes. Both governing ADRs (0018, 0019) are in-tree in this PR.

Notes For Reviewers

  • The read path is the reviewable core: citation/openlibrary.rs (resolve), citation/search_inside.rs (grounding), and their wiring in citation/page.rs. The ADR-0018 invariants worth checking: no request ever hits an import-on-miss endpoint, and a similar-edition scan is never used for verification.
  • citation/openlibrary_apply.rs is mechanism-ahead-of-enablement by design (ADR-0019 Decision 6): review it as a contract implementation, not a live feature. The form adapter's fail-closed rules (unknown input kinds, non-empty target slot, missing _comment slot → refuse) are the load-bearing part.
  • Enrichment candidates are derived conversions only (ISBN-10↔13 check-digit arithmetic); cited values are never relayed verbatim into a proposal, and a proposal pins base_revision so drift refuses client-side before any lane is chosen.
  • The merge with main resolved overlapping seams with Add citation repair/insertion action row to the Citations tab (PRD-0014) #109 by union; the two lanes share BlockRef/CitationFinding without interacting.

🤖 Generated with Claude Code

https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY


Generated by Claude Code

claude added 10 commits July 7, 2026 23:47
…ce 1)

Draft ADR-0018, the Open Library / Internet Archive read contract spawned
by PRD-0009 (Q6a): identifier-gated resolution, a strictly side-effect-free
Books API catalog lookup (never the import-on-miss /isbn/{isbn}.json),
Read API exact-vs-similar scan availability, and the search-inside
grounding-source rules including the scoped ShortBody bypass.

Implement the first read-only slice:

- sp42-platform: BookIdentifier (ISBN checksum-validated, OCLC, LCCN
  LC-normalized, OLID canonicalized with author ids rejected) and
  BookSource; BlockRef carries book_sources alongside cited sources.
- sp42-parsoid: cite-template data-mw extraction now also reads
  isbn/oclc/lccn/ol params (and the cited page), independent of the
  url= gate, so a URL-less {{cite book}} finally yields something.
- sp42-citation extract: a url-less ref with a validated identifier is
  skipped as BookSource (with its identifiers) instead of blending into
  NonUrlSource; the page report prints those identifiers.
- sp42-citation openlibrary: pure build/parse pairs for the Books API
  catalog lookup and the Read API scan availability, partitioned so only
  exact-match scans can ever feed grounding.

Wiring resolution into the verify path and report is the next slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
The page orchestrator now resolves every skipped book ref through the
side-effect-free Open Library lane (ADR-0018) and the report shows what
the catalog said, so a book citation prints a resolved record instead of
a bare identifier.

- sp42-citation openlibrary: resolve_book_source executor — tries a
  ref's identifiers in template order against the Books API, then checks
  Read API scan availability on the first hit. A clean miss is NotFound
  (never a create), a transport failure is LookupFailed (existence
  unknown), and an availability failure degrades to scan-unknown without
  losing the catalog hit. A recording-client test asserts the whole lane
  addresses only /api/books and /api/volumes/brief, never the
  import-on-miss /isbn/{isbn}.json path.
- verify_page: a bounded (<=3, REST politeness) resolution pass over the
  skipped refs' book sources; failures stay inside the books lane and
  never fail the page. Report gains book_resolutions plus
  books_resolved/books_not_found/book_lookups_failed stats, all
  serde-default so older reports still deserialize.
- Renderer: a Books section (resolved record with title/authors/record
  URL and an exact/similar_only/none/unknown scan label; distinct
  not_found vs lookup_failed lines) and a books lead tally.
- SkippedRef now carries its BookSource structures (identifiers + cited
  page) instead of a flattened identifier list, so resolution keeps
  per-template identifier order and the cited page for Layer 2.

Search-inside grounding (Layer 2) is the next slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Layer 2 of the book lane (ADR-0018 Decision 4): a resolved book citation
with an exact-edition scan now gets a real verdict instead of a skip.

- sp42-citation search_inside: the Internet Archive grounding source —
  item-metadata read for the designated search server (texts-only gate),
  BookReader full-text search build/parse (verbatim OCR snippets with
  highlight markers stripped, page numbers kept), conservative claim→query
  term selection, page-anchored deep links, and prepare_book_grounding
  mapping every degraded outcome onto the ADR-0007 split: no usable body
  (not a text item / no index) -> SourceUnavailable; an indexed scan
  searched with zero snippets -> not_supported, never SourceUnavailable.
  Cited page first, whole-book fallback, per PRD-0009 Q5.
- verify: FetchedSource gains a book_snippet provenance flag whose only
  effect is bypassing the generic ShortBody floor at the usability gate —
  arbitrary short web pages still short-circuit, an empty snippet stays
  unusable (covered by a three-way test). CitationFinding gains optional
  BookScanProvenance (ocaid, scanned vs cited page, refined note) and two
  no-model book outcomes for the unavailable/searched-empty cases.
- extract: a url-less ref with validated identifiers is now a BookUseSite
  (same sentence attach as URL refs, shared ordinal sequence) instead of
  an unconditional skip; only identifier-less refs skip at extract time.
- verify_page: the book pass resolves each site against Open Library,
  grounds resolved books through the snippet body and the model panel
  (resolved record as context-only metadata sidecar), degrades similar-
  only/no-scan books to an honest SourceUnavailable pointing at the OL
  record, and re-skips catalog misses with the refined BookSource reason.
  The scanned page the passage actually located on is recorded so
  pagination mismatches surface instead of causing false negatives.
- Renderer: finding lines show scan/scanned_page/cited_page/note; the
  Books section and stats are unchanged from slice 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Answers the open question blocking the apply-contract ADR: what write
mechanism an ordinary operator account can actually use in production.

Key findings (primary sources: openlibrary-client, infogami's API plugin,
the addbook.py form handler, the Writing Bots policy, upstream issues):

- The PRD's "REST save PUT is internal/localhost-only" premise was wrong
  in its reason, right in its conclusion: the PUT and /api/save_many DO
  run against production (the official client and the whole bot ecosystem
  use them), but infogami's can_write() gates every API write on
  membership in /usergroup/api or /usergroup/admin — an ordinary account
  gets 403 even for a single-record save.
- Ordinary accounts write through the website form path
  (POST /books/OL...M/edit, SaveBookHelper): logged-in + per-record
  permission only, edition./work. field prefixes, _comment preserved,
  no CSRF token today, a v revision parameter but no server-side
  refuse-on-conflict (drift protection must be client-side).
- The API usergroup is obtainable by a sanctioned human process (ask the
  admins; bot naming conventions for bulk accounts), and membership is
  publicly readable at /usergroup/api.json — a write-free capability
  probe.
- Auth for either lane: POST /account/login with credentials or
  per-operator IA S3 keys -> session cookie. No published rate limits;
  the policy is an open upstream discussion.

Implication recorded for the ADR: a two-lane apply contract — the
authenticated form POST as the default lane every operator has, and the
REST PUT as a privileged lane for API-usergroup members — both
per-operator, both proposal-only until the ADR and a one-time live form
spike land. PRD-0009's three localhost-only mentions corrected to the
verified usergroup-gating mechanism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
The apply-contract ADR the enrichment lane was waiting on, built on the
2026-07-08 apply-path research:

- Two apply lanes selected by derived capability, never configuration:
  the authenticated edit-form POST every ordinary operator account has,
  and the REST PUT for operators in the API usergroup (membership read
  from the public /usergroup/api.json — a write-free probe).
- Per-operator session via IA S3-key login, held under the ADR-0002
  local-session posture; no shared identity.
- ADR-0010's propose/confirm/refuse-on-drift discipline re-implemented
  client-side around the record revision, since upstream refuses nothing
  on conflict; one field per apply; full audit of every attempt.
- The form lane is a versioned adapter that GETs the form first, replays
  hidden fields, and fails closed on any contract drift (proposal-only
  rather than a best-guess POST to a public catalog).
- The write lane ships disabled behind an explicit enablement gate: a
  one-time live form spike, upstream courtesy contact, and the operator
  capability report.

Cross-references updated: PRD-0009 spawned-ADRs header, the research
note's status, and the STATUS.md book-lane bullet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Review outcome: the usergroup-list derivation guarded against a failure
that is already benign (a pre-mutation 403) while adding a second
unofficial contract that mirrors server permission internals. Replaced
with the server's own answer: attempt the REST lane, fall back to the
form lane on 403 within the same apply, cache the discovered lane per
operator session. Members never pay an extra request; non-members pay
one refused request per session; the 403 handler is the permanent
correctness mechanism and the cache only an optimization, so no state
can be wrong. The fallback re-runs the drift re-read before the form
submit.

Also strengthened the fail-closed form adapter with a post-apply
read-back (verify the confirmed field holds the proposed value; surface
anything else loudly), updated the capability-report wording for
discovery-on-first-apply, and recorded both rejected selection
mechanisms (membership-list derivation, operator config knob) in
Alternatives. Research note's implication section updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
…bled)

The enrichment lane exists as mechanism + fixture tests only: no server
route or CLI surface performs a write, and ENRICHMENT_WRITE_LANE_ENABLED
is the constant (false, with a tripwire test) any future surface must
check per the ADR's enablement gate.

- sp42-citation enrich: pure proposal computation. Deterministic
  ISBN-completion candidates (ISBN-13 from ISBN-10 and back, checksum
  arithmetic on values the record or citation already carries — never a
  verbatim relay of wiki content); raw-record parsing that pins the
  revision; EnrichmentProposal with the replacement value, provenance,
  comment, and the content hash a confirmation binds to. A gap that is
  closed on the raw record yields no proposal.
- sp42-citation openlibrary_apply: the two-lane apply. S3-key login ->
  session cookie; REST PUT carrying the full updated record + _comment;
  the form-lane adapter that GETs the edit form, parses it fail-closed
  (unknown widgets, ambiguous selects, a missing target or _comment slot,
  or an occupied gap all refuse as contract drift), replays every field
  with one changed, and checks the redirect success shape; the executor
  with drift re-read, 403 fallback that restarts the drift window and
  caches the discovered lane per session, and a post-apply read-back
  that verifies the exact proposed value landed before reporting success.
- Books report: resolved entries now list the read-only enrichment
  candidates (propose=isbn_13:...), PRD-0009's proposal-listing surface.

Covered by tests: lane discovery and caching (no PUT after a cached form
lane), refusal-before-write on a moved revision, contract-drift refusal
with nothing posted, read-back mismatch surfacing as failure, and the
complete-record-yields-no-proposal rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Manual, network-touching, never CI (ADR-0009 keeps tests on fixtures).
Walks the exact read chain verify-page uses — Books API catalog lookup,
Read API scan availability, archive.org item metadata, BookReader
search-inside — with read-only GETs and asserts precisely the response
fields the PRD-0009 parsers read. A pass means production still matches
the fixtures; a failure names the drifted contract and keeps the real
payload for fixture refresh. By construction it never touches the
import-on-miss /isbn/{isbn}.json path or any write endpoint.

Header notes that Open Library 403s datacenter IPs (observed from this
environment), so the smoke runs from a developer connection — the same
caveat applies to any future cloud-hosted SP42 deployment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Review outcome: no local Open Library instance will be set up. The
enablement spike's form-contract validation moves to a zero-write
production capture — the edit-form GET is read-only, so the real HTML
can be fetched under a session cookie and committed as the adapter's
fixture, putting parse/fill against real markup in the normal test
suite. POST semantics (success shape, _comment in history, stale-v
behavior) stay in the one-write phase of the spike as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
Brings in the Wikidata entity read path (#119), the citation
repair/insertion browser surface (#109), the architecture map + CI
freshness gate (#118/#139), coordination fail-closed (#146), the
as-built ADRs 0020-0023, and the docs attic move.

Conflicts were the book-citation lane (this branch) and the bare-URL
repair lane (main) touching the same seams; resolved by union:

- BlockRef carries both book_sources (branch) and is_bare_url_ref
  (main); parsoid threads RefSources and the ref_bare map side by side.
- CitationFinding carries both book_scan and is_bare_url_ref.
- verify_page keeps the books pass; main's re-verify surface
  (select_reverify_use_site / apply_reverified_finding) now tallies
  through the five-argument tally_stats with book resolutions intact.
- STATUS.md keeps both progress bullets.

Regenerated docs/platform/architecture.md for the new citation modules
(openlibrary, search_inside, enrich, openlibrary_apply).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u5MVp3vAN44KhsVg3vroY
@tieguy
tieguy requested a review from schiste as a code owner July 12, 2026 19:54
@tieguy
tieguy requested a review from schiste July 12, 2026 19:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47d7ef78d0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +58 to +59
if segments.next() != Some("details") {
return None;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle Read API stream item URLs

When the Open Library Read API returns an exact scan whose itemURL uses /stream/<ocaid> (the official Read API example uses this shape: https://openlibrary.org/dev/docs/api/read), this check rejects it because only /details/ URLs are accepted. In that case verify_book_use_site sees no groundable_ocaid and reports SourceUnavailable instead of grounding a readable exact scan, so exact book citations can be skipped despite having usable IA OCR.

Useful? React with 👍 / 👎.

@tieguy

tieguy commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #147 + #148.

This branch (claude/openlibrary-resolve) is the original combined
implementation of PRD-0009; #147 (read path) and #148 (apply lane) are its
split, rebased successors. A file-by-file comparison of the book-lane content
(all of citation/{openlibrary,openlibrary_apply,search_inside,enrich, extract,page,verify,storage}.rs, the report/parsoid/mcp touchpoints, the
smoke script, and the ADR/PRD docs) found the successor stack carries all
of this branch's content — every function, all 119 book-lane tests, and the
doc bodies — with no drops. The only differences are ADR renumbering and
neutral reorderings.

Two things make merging this branch instead actively wrong:

  1. Its ADR numbers collide: ADR-0018 is claimed by PR docs(references): add PRD-0017 and ADR-0018 for interactive review sessions #151 (review sessions)
    and ADR-0019 by PR Docs: cached page re-verification — PRD-0014 amendment + ADR-0019 #123 (cached re-verification); the successors
    renumbered to 0024/0025.
  2. It lacks the 2026-07-12 Open Library contract-drift fix on Ground book citations against Open Library / Internet Archive (read path, PRD-0009) #147
    (f47dfba): the Read API no longer puts archive.org/details/<ocaid> in
    itemURL, so this branch's search-inside grounding never fires against
    live Open Library.

Closing in favor of #147 (review in progress) and #148 (stacked follow-up).

@tieguy tieguy closed this Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants