Library-aware recommendations + series cards#3
Merged
Conversation
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
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
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
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
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
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
…notes 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
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
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
matching.py(normalize_title/match_key)./api/availabilityand every discover row now carry normalized match keys so already-owned / actively-requested recommendations are recognized. ISBN-exact first, normalizedtitle + authorfallback. Backend emits the keys; the frontend does dumb set-membership — no normalization duplicated in JS, so the two sides can't drift.select_continue_seriesnow returns grouped{series_id, series_name (cleaned via new clean_series_name), entries[{book, position, released}]}. Unreleased installments are included and flagged, not dropped. NewrenderSeriesCardfront-end component with ‹ › arrows that opens on the next not-fully-owned entry and hides a fully-owned series.Constraints honored
app.pystays a thin router; logic inmatching.py/recommendations.py/hardcover.py.hardcover_enabledgate and safe degradation preserved (rows never 500).Testing
ruff check .clean;pytestgreen (83 passed). New unit tests:normalize_title/match_key,clean_series_name, groupedselect_continue_series(unreleased flagged, canonical pick, per-card cap, recency order, cleaned name),/api/availabilitynormalized keys, grouped discover contract.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