Skip to content

fix(series): keep a duplicate row off its neighbour in the diff (#2410) - #2606

Draft
vavallee wants to merge 2 commits into
mainfrom
fix/2410-series-diff-duplicates
Draft

vavallee wants to merge 2 commits into
mainfrom
fix/2410-series-diff-duplicates

Conversation

@vavallee

@vavallee vavallee commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

A second local row for a book that already bound in the Hardcover diff could fall through to a similarly titled catalogue entry. Two rows for The Way of Kings (ebook and audiobook) left the second one bound to The Way of Kings Prime at 0.90, so Prime showed as owned and dropped out of Missing and series fill. #2557's position rule only covered this when both the row and Prime carried different stored positions. Closes #2410.

The rule

buildHardcoverDiff in internal/api/series.go, title pass only. The identity pass from #2557, bestCatalogMatch, thresholds and the output order of every list are unchanged.

  1. Title matched locals bind in order of their best score, highest first. Ties keep library order.
  2. A local whose best entry is taken falls through to its next best exactly as on main, unless it is a second copy of a bound book (boundCopyOf): some bound entry it scores above that fallback is held by a local with the same title, meaning both titles carry the same set of numbers and TitleScore(local, holder) is at least the local's score on that entry. Then it is Local only, logged at DEBUG as series diff: local book is a second copy of a book already bound, left Local only.
  3. Every bound entry the local scores above its fallback is checked, not only its best, so a copy whose best entry went to a different book is still caught through its twin.

The first version of this PR refused any weaker fallback. Review showed that starved real books on libraries without stored positions, which is worse than the bug: each bare "He Who Fights with Monsters N" scores 0.96 against the umbrella entry volume 1 holds and 0.90 against its own, so 11 of 13 rows went Local only, 10 owned volumes showed as Missing, and a real Fill created Wanted copies of volumes 2 and 9.

Results

TestHardcoverDiffRealisticSeriesNeverStarves, every series in both library orders, counts summed over both orders. Cells are misbound / starved / owned entries listed Missing. Starved means Local only with no other copy of the same book bound.

Series Main PR before After
#2553 library, with positions (with and without the vol 9 id) 0 / 0 / 0 0 / 0 / 0 0 / 0 / 0
#2553 library, no positions, vol 9 carries its id 3 / 1 / 0 0 / 2 / 2 2 / 0 / 0
#2553 library, no positions, no ids 5 / 1 / 0 2 / 2 / 2 2 / 0 / 0
HWFWM 1 to 12 plus a second 12, with positions 0 / 0 / 0 0 / 0 / 0 0 / 0 / 0
HWFWM 1 to 12 plus a second 12, no positions 1 / 1 / 0 0 / 22 / 20 0 / 0 / 0
Defiance of the Fall, with positions 0 / 0 / 0 0 / 0 / 0 0 / 0 / 0
Defiance of the Fall, no positions 4 / 0 / 0 1 / 1 / 1 2 / 0 / 0
The Primal Hunter, with positions 0 / 0 / 0 0 / 0 / 0 0 / 0 / 0
The Primal Hunter, no positions 6 / 1 / 0 1 / 2 / 1 2 / 0 / 0
Stormlight, with positions 2 / 0 / 0 0 / 0 / 0 0 / 0 / 0
Stormlight, no positions 2 / 0 / 0 0 / 0 / 0 0 / 0 / 0
Fill on HWFWM without positions, books created 0 2 per order, 2 queued 0

The test asserts starved and owned Missing, not misbound. It fails 4 diff subtests on main (#2553 without positions in library order, both id variants; HWFWM without positions reversed; Primal Hunter without positions in library order) and 8 diff subtests plus both fill subtests on the previous rule. TestHardcoverDiffDuplicateRowNeverBindsANeighbour (60 subtests) and the #2553 test pass unchanged.

Known limits, also on main

Still misbound after this change, all from title similarity, all misbound on main too:

  • He Who Fights with Monsters series comparison matches catalogue entries to the wrong volumes #2553 library without positions, library order: "He Who Fights with Monsters: A LitRPG Adventure" scores 0.93 against volume 4's entry and takes it, and "He Who Fights with Monsters 4" then takes volume 1. A swap, nothing starved or Missing. Main misbinds the same library (duplicate 12 row on volume 4, real volume 4 Local only).
  • Defiance of the Fall without positions, reversed: volume 1 takes 16 and volume 16 takes 1.
  • The Primal Hunter without positions, reversed: volume 13 ties with volume 1 on catalogue "The Primal Hunter 1" (both 0.90) and takes it by library order, and the second volume 1 row then takes 13. The issue's order dependence remains for exact ties.
  • Stormlight "Book One" (probe only): a second row titled "The Way of Kings: Book One of the Stormlight Archive" at position 1, with Prime also at position 1, still takes Prime. The position floor lifts Prime to 0.90, equal to its score on The Way of Kings, so there is no entry it scores above its fallback to compare.

Other shapes probed but not committed: Defiance with bare local titles, Defiance with an umbrella volume 1 entry, and Stormlight with a Prime row owned all show zero starved and zero owned Missing here. HWFWM with volume 1 titled "He Who Fights with Monsters 1", no positions, reversed, gives 3 misbound here against 1 misbound plus 1 starved on main: every bare title ties on the umbrella entry and library order decides.

Checklist

  • Tests added or updated
  • Doc-update gate cleared (docs/Hardcover-Series-Wiki.md, binding section, now states the limits instead of order independence)
  • Wiki pages updated if user-facing behaviour changed
  • Changelog fragment changelog.d/2410-series-diff-duplicates.md

Test plan

  • go build ./... && go vet ./...
  • go test ./internal/api -count=1
  • golangci-lint run ./internal/api/... (v2.11.4, 0 issues)
  • New test run against main and the previous rule with go test -overlay (results above)
  • cd web && npm run build (no web change)

🤖 Generated with Claude Code

https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9

The Hardcover diff let a local whose best catalogue entry was already
taken fall through to its next best on title similarity alone. With two
rows for The Way of Kings, an ebook and an audiobook, the second bound to
The Way of Kings Prime at 0.90, so Prime showed as owned and dropped out
of Missing and series fill. #2553's position rule only caught this when
both the row and Prime carried different stored positions: 45 of the 54
variants in the new test still misbound on main.

Title matches now bind strongest first, and a local only falls through
to an entry it scores exactly as well as the one it lost. Otherwise it
is Local only, the outcome #2553 already gives a duplicate at one stored
position. An equal score still falls through, which is the #2343 tie.
Output order and thresholds are unchanged.

Signed-off-by: vavallee <vavallee@protonmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/api/series.go 96.15% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

The previous commit listed a local as Local only whenever its best
catalogue entry was taken and its fallback scored lower. On libraries
without stored positions that starved real books: each bare "He Who
Fights with Monsters N" scores 0.96 against the umbrella entry volume 1
holds and 0.90 against its own, so 11 of 13 rows went Local only, 10
owned volumes showed as Missing, and a real Fill created Wanted copies
of volumes 2 and 9.

A local is now Local only just when a bound entry it scores above its
fallback is held by a local with the same title: both titles carry the
same set of numbers, and their TitleScore is at least the local's score
on that entry. Every such entry is checked, not only the local's best.
Otherwise it falls through exactly as on main. The identity pass,
bestCatalogMatch, thresholds and output order are unchanged.

TestHardcoverDiffRealisticSeriesNeverStarves runs the #2553 library, a
full He Who Fights with Monsters library, Defiance of the Fall, The
Primal Hunter and Stormlight, with and without positions, in both
library orders, plus a real Fill that must create nothing. It fails 8
diff subtests and both fill subtests on the previous rule, and 4 diff
subtests on main.

The wiki no longer claims order independence. Exact ties still bind in
library order, and a second copy with a different title still takes
Prime when the position floor lifts Prime to a tie.

Signed-off-by: vavallee <vavallee@protonmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
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.

Hardcover series diff assigns catalog entries in library order, so a local book can bind to one it is not

1 participant