fix(comicvine): no-year path checks volumes individually, removing nameMatch gate (bookshelf-c51j) #980
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-c51j"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
searchStructuredNoYearused a batch OR-filter gated bynameMatch=true, excluding volumes like "Spectacular Spider-Man" (nameMatch=false, Jaccard=0.50 with query) that passed the relevance floor and had the requested issue. A broader (no-year) query thus returned zero results when the year-known path found candidates.fetchIssuesByFiltercalls per scored volume (mirroring the year path), capped atmaxNoYearVolumesToCheck=10. No nameMatch gate is applied — any volume that passes the relevance floor (Jaccard >= 0.30 OR nameMatch=true) is checked individually.fetchIssuesBatchByFilter(now dead code).Test plan
make testpasses,make coveragegreen (100%).Closes bead bookshelf-c51j on merge.
bc2646acc6e038b379f0Security Review — bookshelf-c51j (ComicVine no-year search algorithm)
Four targeted checks performed against the diff (
searchStructuredNoYearrefactor +structured_test.go):1. SSRF / unvalidated URL fetch
baseURLis injected at construction time from server config, never from user input. Volume IDs passed tofetchIssuesByFilterareint64values sourced from the ComicVine API response and formatted with%d— no user-controlled URL fragment is introduced. URL construction usesurl.Values.Set()(properly encoded). No new SSRF surface.2. Injection into the filter string
The filter is built as
fmt.Sprintf("volume:%d,issue_number:%s", volumeID, issueNumber). ThevolumeIDisint64(%d— injection-proof). TheissueNumber(norm) is the output ofmetadata.NormalizeIssueNumber, which was the pre-existing code path — unchanged by this PR. That normalization and thefetchIssuesByFiltersignature both predate this diff; the PR only changes the call site from a batch loop to an individual loop. No new injection surface.3. Secrets / PII in logs
The
log.Infocall infetchIssuesByFilterlogsvolume_id(int64) andissue_number(normalized string). The API key is added tourl.Valuesbut never logged. Clean.4. Amplification / rate-limit bypass
The old code made 1 batch call covering up to 50 volumes. The new code makes up to 10 individual calls (cap is the compile-time constant
maxNoYearVolumesToCheck = 10, reduced from 50). Each call goes through the samelimiterandhourly *rate.Limiterpassed todoGetWithRetry— so all 10 calls are individually rate-limited. The fan-out is hard-capped and rate-limited; no unbounded amplification.No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
CODE REVIEW — bookshelf-c51j (PR #980)
Phase 0: DEMO
No interactive DEMO block — this is a logic fix verified by unit tests. CI is green per the implementer. Proceeding.
Phase 1: Spec Compliance
Spec required:
searchStructuredNoYearbatch+nameMatch-gate with per-volumefetchIssuesByFiltercalls (mirroring year path) ✓maxNoYearVolumesToCheck=10✓fetchIssuesBatchByFilter✓Phase 2: Code Quality
1. CORRECTNESS — verified ✓
Old code gated
batchIDsonsv.nameMatch, so a volume whose Jaccard ≥ 0.30 but nameMatch=false was silently excluded. The fix iteratesscored(already relevance-floored insidescoreVolumes) unconditionally up to the cap, mirroring the year path exactly. The no-year result set is now structurally guaranteed to be ≥ year result set for the same query.2. REGRESSION TEST — genuine ✓
Vol 500 ("Amazing Spider-Man", nameMatch=true) has no issue #5; Vol 501 ("Spectacular Spider-Man", nameMatch=false, Jaccard=0.50) has issue #5. Old code:
batchIDs=[500]only → batch returns empty → ErrNoMatch. New code: queries 500 (empty) then 501 (hit) → 1 result. Test assertsHaveLen(1)which would fail on old code and pass on new. The 5 updated existing tests are correctly rewritten for the new mechanism — none are loosened.3. DEAD CODE — fully gone ✓
git grep fetchIssuesBatchByFilter origin/bd-bookshelf-c51jreturns empty. No stale references.4. MERGE ARTIFACT (hd0y.1
associated_images) — intact ✓associated_imagespresent infield_listat 3 call sites insearch.go(lines 416, 689, 735). Rebase did not drop them.5. PACKAGE/COVERAGE — clean ✓
package comicvine_testat top ofstructured_test.go.git diff origin/main -- scripts/check-coverage.shis empty — no new exclusions.[MINOR]
internal/metadata/comicvine/scoring.go(sort comparator, end ofscoreVolumes) — sort is effectively a total order only by coincidenceThe insertion sort uses strict
>(stable: ties preserve input order). Ties are prevented in practice becausevolumeSearchLimit=25exactly matches the rank-bonus range (ranks 0–24 → bonuses 25–1, all unique). IfvolumeSearchLimitwere bumped above 25 without adding an explicit tiebreaker (e.g.,, || (score equal && vol.ID < other.vol.ID)), volumes at ranks ≥25 would all get rank-bonus=0 and could tie on total score, making the top-10 cut order-dependent on input. Currently sound; worth hardening at a future refactor.[MINOR]
internal/metadata/comicvine/search.go:150—MaxProviderResultssoft-cap removed from the no-year accumulation loopOld code broke when
len(results) >= metadata.MaxProviderResults(12). New code only breaks ati >= maxNoYearVolumesToCheck(10). WithfetchIssuesByFiltercapped atlimit=5per call, the theoretical max output is 50 results (10 volumes × 5 issues) vs the old 12. In practice issue-number queries return 0–1 result per volume, so real-world output stays ≤10. No user-visible impact today, but the cap was silently dropped.REVIEW VERDICT: 0 blocker, 0 major, 2 minor