fix(importer): suggest every book by the matched author when adopting a file (#2879) - #2883
Conversation
… a file (#2879) The author spelling was not the problem. "Sarah K. L. Wilson" and "Sarah K.L. Wilson" already resolve to the same catalogue author, which is why the three wrong suggestions were all her books. Suggestions on the adoption page were ranked only from the reconcile pool (Wanted, or Imported with a format missing on disk), so a Skipped or already Imported "The Matsumoto" was never a candidate, and the best of the rest scored 0.667, 0.643 and 0.622. For a resolved author, suggestions now come from every catalogue book of that author, whatever its status. Excluded books stay out, as before (the scan's book list never had them). An exact title ranks first, and between equal scores a reconcilable book goes ahead. isReconcileCandidate and the 0.85 automatic claim are unchanged, so the scan still never attaches a file to a Skipped book or one that already has its file. Authorless files keep the old pool. Adopting into a book that already has a live file of that format adds the new file alongside it (book_files is additive, the existing file keeps rendering) and the response now says so. In the web, every suggestion shows its status, a Skipped or Imported suggestion is never a one click Confirm, and the editor notes when the chosen book already has its files. Signed-off-by: vavallee <vavallee@protonmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
There was a problem hiding this comment.
Overall clean fix. Root cause is correctly identified, the wantedBooks/catalogue split is sound, and the test coverage is solid. A few observations.
Logic
adoption_adopt.go:172–181 — hasLiveFile runs before register, so the message reflects the book's state at check time, not at completion time. A concurrent deletion in that window means the message fires without a true second copy being left. Data integrity is unaffected (the file still gets registered), but the informational message would be wrong in a race. Low likelihood, but worth knowing.
adoption_adopt.go:133 — checkOwnership(…, 0) correctly blocks the untracked file being already owned by any book before the book is resolved; the second check in register at line 337 uses bookID (allowing re-registration to the same book). The TOCTOU window between the two checks was pre-existing.
Frontend
AdoptionEditor.tsx:656 — the alreadyImported note only fires for status === 'imported'. A user who opens the editor and selects a Skipped suggestion gets no advisory that the book was deliberately skipped and the adoption will give it files. The status pill is visible, but a short note parallel to the imported one would reduce surprises. Not a blocker.
AdoptionRow.tsx:10 imports StatusPill from AdoptionEditor.tsx. No circular dependency, but the import direction is unusual (Row → Editor). If this component ever needs to be used elsewhere components/ would be the natural home.
No issues with:
- The
wantedBooks/catalogueseparation keeping the auto-reconcile path unchanged. - Lazy initialisation of
catalogueByAuthor(zero cost when nothing is unmatched). - Tie-break (reconcilable wins on equal score).
alreadySettledcorrectly blocking one-click Confirm forimportedandskipped.- The Undo-of-Skipped limitation is documented in the PR body and in scope for a follow-up.
— 🤖 Bindery triage bot (automated). Reply to correct me; a human will see it.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Closes #2879
Summary
The author's initials were not the cause.
textutil.NormalizeAuthorNamealready folds "Sarah K. L. Wilson" and "Sarah K.L. Wilson" to the same key, and the scan's author resolution (matchingAuthors/resolveAuthorsoverauthorNameTokens) matched the folder author to the catalogue author. That is why all three wrong suggestions were her books. The new test reproduces the report exactly: on main the unit is stored with Paths of Deception (0.667), Chase the Moon (0.643) and Mist of Power (0.622), all by the author resolved from the "K. L." folder. A Wanted "The Matsumoto" in the same layout is claimed automatically at jw=1 on main too.The real cause:
rankCandidatesscored onlywantedBooks, the reconcile pool fromisReconcileCandidate(Wanted, or Imported with a format missing on disk). "The Matsumoto" would score 1.0, so it was not in that pool: Skipped, or already Imported with a file that still exists. The reporter's log fits the second case: files under bothSarah K.L. Wilson/andSarah K. L. Wilson/with Calibre style(13105)folder ids and_for:. Bindery's renamer renders{Author}fromauthor.Namewith({Year})folders, so it produces one consistent folder per author; those two spellings came from another tool.Change 1, suggestion pool (
unmatched_units.go,scanner.go): for a resolved author, suggestions are ranked from every catalogue book of that author, whatever its status. Built lazily, only when the scan has unmatched units. An exact title scores 1 and ranks first; between equal scores a reconcilable book goes ahead. Excluded books are still never suggested (the scan'sBookRepo.Listfiltersexcluded = 0, the existing convention). Files with no readable author keep the old pool, since the whole library is too wide without an author. Cap stays at 3.isReconcileCandidateand the 0.85 automatic claim are unchanged.Change 2, the outcome when the chosen book already has a file (
adoption_adopt.go): adoption was already additive (AddBookFileIfMissingappends a book_files row,derivedFormatPathkeeps rendering the lowest id live row, so the existing file stays the one shown; Undo removes only the added row). That is the outcome, now made explicit: the adopt response carries a message when the book already had a live file of that format.Change 3, web (
adoptionMatch.ts,AdoptionEditor.tsx,AdoptionRow.tsx): each suggestion in the editor shows the book's status pill (bookStatusBadge), and the row shows it for an Imported or Skipped top suggestion. Such a suggestion is never a one click Confirm, only a Possible match that opens the editor, where a note says the book already has its files. After adopting, the row says the file was added alongside.Not done, stated:
MatchAuthorNameuntouched.How it was verified
TestScanLibrary_SuggestsTheExactTitleWhateverItsStatus(catalogueSarah K.L. Wilson, file atSarah K. L. Wilson/The Matsumoto (13110)/The Matsumoto - Sarah K. L. Wilson.epub), subtests Skipped and Imported with an existing file elsewhere: The Matsumoto is the first suggestion with an exact score, the unit is stored (not auto reconciled), the book's status and monitored flag are unchanged, and the file is not registered on it.TestScanLibrary_WantedExactTitleStillReconciles: a Wanted exact title is still claimed automatically, no unit.TestRankCandidates_ReconcilableBookWinsATie.TestAdopt_IntoABookThatAlreadyHasItsFile: 200, two ebook rows, still Imported, still rendering the existing file, message set; Undo leaves only the original and the book Imported; nothing touched on disk.TestAdopt_IntoAWantedBookSaysNothingExtra.adoptionHint.test.ts(Imported or Skipped is never strong),AdoptionView.test.tsx(Imported pill, no Confirm, editor note, "Added alongside" outcome).candidates = [{BookID:1 Score:0.667} {BookID:3 Score:0.643} {BookID:4 Score:0.622}], want The Matsumoto (book 2) first, the issue's three books. The Wanted test passes on main, as it should. Both web tests fail on main's sources (expected 'strong' to be 'possible';Unable to find ... Possible match). The adopt test does not compile on main (no message constant); its file and undo assertions describe main's existing additive behaviour.isReconcileCandidate: both scan subtests fail with the three wrong suggestions.isReconcileCandidatewidened to Skipped and Imported: both scan subtests fail withunits = [], want the untracked Matsumoto left for a person to decide.TestRankCandidates_ReconcilableBookWinsATiefails.go build ./... && go vet ./...clean;go test ./cmd/... ./internal/...exit 0;golangci-lint run ./internal/importer/... ./internal/api/...0 issues. Web:tsc --noEmitclean, eslint on touched files clean, fullvitest run1175 passed,npm run buildexit 0.Checklist
docs/User-Guide-Wiki.md, adoption section)changelog.d/2879-adoption-suggestion-pool.md)🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9