Skip to content

Library-aware recommendations + series cards#3

Merged
Garrett3Nelson merged 34 commits into
mainfrom
feature/hardcover-recommendation-rows
Jul 4, 2026
Merged

Library-aware recommendations + series cards#3
Garrett3Nelson merged 34 commits into
mainfrom
feature/hardcover-recommendation-rows

Conversation

@Garrett3Nelson

Copy link
Copy Markdown
Owner

Summary

Makes the three Hardcover personal discovery rows library-aware and turns Continue the Series into a navigable per-series card. Builds on the completed Hardcover personal rows work.

Implements the approved spec: docs/superpowers/specs/2026-07-03-library-aware-recommendations-design.md (plan: docs/superpowers/plans/2026-07-03-library-aware-recommendations.md).

Three features

  1. Reliable library matching — new pure, tested matching.py (normalize_title / match_key). /api/availability and every discover row now carry normalized match keys so already-owned / actively-requested recommendations are recognized. ISBN-exact first, normalized title + author fallback. Backend emits the keys; the frontend does dumb set-membership — no normalization duplicated in JS, so the two sides can't drift.
  2. Series cardsselect_continue_series now returns grouped {series_id, series_name (cleaned via new clean_series_name), entries[{book, position, released}]}. Unreleased installments are included and flagged, not dropped. New renderSeriesCard front-end component with ‹ › arrows that opens on the next not-fully-owned entry and hides a fully-owned series.
  3. Owned / format-aware behavior — flat rows badge partial ownership and hide only when fully owned across configured slots; the request modal marks an owned slot "In library" and pre-selects the missing format.

Constraints honored

  • No new dependencies.
  • app.py stays a thin router; logic in matching.py / recommendations.py / hardcover.py.
  • Hardcover token never exposed to the frontend; hardcover_enabled gate and safe degradation preserved (rows never 500).

Testing

  • ruff check . clean; pytest green (83 passed). New unit tests: normalize_title/match_key, clean_series_name, grouped select_continue_series (unreleased flagged, canonical pick, per-card cap, recency order, cleaned name), /api/availability normalized keys, grouped discover contract.
  • Browser-verified against the live account: clean series names; arrows page and clamp at the ends; cards open on the correct next-gap entry; "Red God (2026)" shows a Coming 2026 badge and is not requestable; owned eBook ✓ badge on flat and series cards; hide-fully-owned flat card and hide-fully-owned series; modal marks the owned slot In library.

Known limitation (see TODO.md)

The live account has only the ebook slot configured, so the two-slot pre-select-the-missing-format path could not be exercised end-to-end (partial ownership never arises with one slot). The owned-marking half was confirmed. Bookshelf also exposes no ISBN/author, so matching runs on the title-only fallback — the documented graceful-degradation path.

🤖 Generated with Claude Code

Garrett3Nelson and others added 30 commits July 3, 2026 11:59
Continue-the-Series, More-by-Authors, and Want-to-Read discovery
carousels driven by the configured Hardcover account. Brainstorming
complete; API feasibility verified live (depth-3 queries, status ids,
cached_featured_series, series/author expansion, filtering rules).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
…oreign editions

Live Hardcover data exposed gaps in the design's filtering rules:
- book_series was truncated at 40, non-deterministically dropping the English
  canonical (limit raised to 100).
- The most-popular edition of an installment can be a compilation (e.g. a
  novella fix-up). Per-edition filtering dropped it and fell back to a foreign
  edition. Now pick the canonical (by users_count) per position first, then
  filter it — dropping the whole position rather than substituting.
- Filter announced-but-unreleased next books (future release_date) and
  'Untitled ...' placeholders; they aren't requestable.
- Raise by-authors query limit to 60 so exclusions don't starve the row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Also add match_title to shared EXPECTED_KEYS test fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
…er dropped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
…Read badge

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
…rence test

Addresses final-review Minor findings: soften the cap-from-top comment and
fix the stale design-doc 'tail truncated' line to match keep-highest; note
read positions skip content filters; clarify the all-read card-removal comment;
add a test locking the read-edition-over-rank canonical pick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
…series cards

Whole-series entries included read positions (pos <= furthest); since furthest
>= 0, position 0 always qualified as read, and read positions skip the
compilation/noise filters — so Hardcover's position-0 editions (prequels,
omnibus, anthologies, often a foreign-language edition) anchored the card
(e.g. Dune's French 'La Communauté des sœurs', Witcher's 'Sorceleur -
L'Intégrale'). The design scoped entries to positions 1..primary_books_count;
the code guarded only the upper bound. Add the missing lower bound (pos >= 1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
Garrett3Nelson and others added 4 commits July 3, 2026 21:46
… flag

Hardcover's `compilation` boolean is applied inconsistently — it flags some
single novels (e.g. the Witcher's 'The Time of Contempt', 1873 readers) as
compilations. Dropping any position whose top-ranked edition was flagged
compilation silently deleted those installments from the series card while
every other edition at the position was a 0-reader foreign edition.

Detect box sets by title instead: strengthen the box-set regex to also catch
'Complete', 'Collection', and 'N Books Set' patterns, keep the compilation flag
only as a reader-count tiebreaker in _rank, and drop the hard compilation filter
in select_continue_series. Real installments (dominant reader count, clean title)
now survive; genuine box sets are still filtered by title and lose on ranking.
Anthologies are unaffected (Hardcover flags them compilation=False and they sit
at fractional positions outside the primary run).

Verified live + in-browser: The Witcher card now shows positions 1-5 in English
with 'The Time of Contempt' restored at position 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnnLt5oENFMFrjwFXRL3o
…nloaded books

Investigated live against the real Hardcover account and each series' web page.

Series cards (recommendations.py):
- Replace the unreliable `featured` filter with is_partial_book/canonical_id:
  drops split "Part N" volumes (Oathbringer/Wind and Truth Part 1) and merged
  translation editions (Little Women "Dobre żony" 1.2), while restoring real
  featured=False novellas (Old Man's War "The Sagan Diary" 2.5). Verified against
  the live Stormlight/OMW/Murderbot/Little Women pages.
- Don't seed series_progress from a read book cross-linked at a non-numeric
  position (anthology "Press Start to Play" -> The Machineries of Empire at
  "- Gamer's End"), so an unread series is no longer recommended. A genuine
  fractional read (Witcher's "The Last Wish" 0.5) still seeds.
- Add is_partial_book/canonical_id to SERIES_QUERY.

Availability (app.py):
- get_books() returns the whole Bookshelf/Readarr catalog, including fileless
  metadata stubs (editions=null, monitored false, bookFileCount 0) auto-created
  during author refreshes. Skip a book unless statistics.bookFileCount > 0, gated
  before the format dispatch. Fixes false eBook/Audiobook badges (Murderbot 4.5
  "Home" showed Audiobook ✓ though no file exists). See TODO.md for the known
  Bookshelf bookFileCount cache-lag caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWZ6TndJMZaQ7ejrhiXAL9
…s scroll left

nextGapIndex searched from index 0, so cards opened on unread fractional
installments sitting behind the reading frontier (Stormlight 0.1 "Way of Kings
Prime" draft, OMW 1.5). Now it derives the frontier — the highest integer
position among read entries — and lands on the first actionable installment
past it, leaving missed 0.x/x.5 releases as scroll-left context. With no reads
yet it starts at position >= 1 so prequels are skipped and the card opens on
book 1. When caught up on the whole-numbered line (nothing actionable ahead),
it opens on the furthest read whole book so missed minors are still reachable
by scrolling back, rather than jumping to a 0.x prequel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWZ6TndJMZaQ7ejrhiXAL9
…wned badge

The nav (‹ position ›) sat directly after the card body, so a Read/Owned/Coming
badge growing the body pushed the arrows down — misaligning them across cards
and making them jump as you scroll between badged and unbadged entries. Cards
already stretch to equal height in the flex row, so margin-top:auto pins the nav
to the card's bottom edge, keeping the arrows on a fixed baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWZ6TndJMZaQ7ejrhiXAL9
@Garrett3Nelson
Garrett3Nelson merged commit a1603ac into main Jul 4, 2026
4 checks passed
@Garrett3Nelson
Garrett3Nelson deleted the feature/hardcover-recommendation-rows branch July 4, 2026 14:55
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.

1 participant