fix(enrich): add comic search path to bulk EnrichWorkflow (bookshelf-vnu3) #884
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-vnu3"
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
EnrichWorkflowsearched comics as ebooks.RefetchMetadataalways built an ebookMatchQuery(Title/Author/ISBN), so ComicVine received a title-only query and returned no comic metadata. Book 68530 ("The Invincible Red Sonja #7") showed COMPLETED state butcomic_metadatawas never written.RefetchMetadatavia a newis_comicfield inGetBookMetadataForRefetch. WhenIsComic && series != "", build a structured comicMatchQuery(Series + IssueNumber + Year using the same fallback chain as the interactive Fetch Metadata form) and restrict providers to those with"comic-issue"capability (ComicVine, Metron).Metronis now in thebuildEnrichDepsprovider pool (withmetronBulkIntervalrate limit) so comic bulk enrich can use it alongside ComicVine.Changes
internal/db/queries/metadata.sql+internal/db/sqlc/metadata.sql.go: ExtendGetBookMetadataForRefetchto LEFT JOINcomic_metadataand addis_comicEXISTS subquery + 4 comic fields.internal/books/metadata_store.go: AddIsComic,ComicVolumeName,ComicIssueNumber,ComicCoverDate,PrimaryComicFileNametoMetadataRow; map them inGetMetadataForRefetch.internal/books/metadata_service.go: AddComicRefetchSearchInputs(MetadataRow)(exported, tested) implementing the volume_name→series_name→filename fallback chain. UpdateRefetchMetadatato detect comics and build a comic query with capability-filtered providers.internal/app/build_extended_deps.go: Add Metron toallProvidersinbuildEnrichDeps.metadata_service_test.go+metadata_store_test.gowith full coverage of the new paths.Test plan
make testgreenmake coveragegreen (check-coverage: OK — zero uncovered statement blocks)make buildcleanCloses bead bookshelf-vnu3 on merge.
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
COALESCE in the scalar subquery caused sqlc to generate interface{} instead of string, breaking the struct type. Move the CBX filename lookup to a dedicated GetFirstCBXFileNameForBook :one query that returns sql.ErrNoRows when no CBX file exists, avoiding the NULL-scan problem entirely. GetMetadataForRefetch gains a getFirstCBXFileName func dep (5th arg) and calls it conditionally when row.IsComic=true. Warn-logs real DB errors; ignores ErrNoRows (expected for non-comic paths). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
sqlc generates functions in alphabetical order not SQL-file order. GetFirstCBXFileNameForBook ('GetF') belongs after GetCategoryByName ('GetC') and before RefreshSortAuthorName ('Ref') not after GetBookMetadataForRefetch or at end of file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Security Review — PR #884 (bookshelf-vnu3)
Focus: comic enrich path — filename parsing, Metron credentials, SQL safety, outbound injection, multi-user, architecture boundary.
No blockers. No majors. Three minor test-convention findings.
[MINOR] internal/books/metadata_service_test.go:214 — Multiple Expect calls per It in ComicRefetchSearchInputs tests
Each Context/It block for ComicRefetchSearchInputs (lines ~221-227, ~237-241, ~250-253) contains setup, invocation, and 2-3 Expect calls inside a single It block. Project convention (project-conventions.md) requires exactly one Expect per It with setup in BeforeEach and invocation in JustBeforeEach. Fix: hoist the row construction into BeforeEach, the call into JustBeforeEach, and split into separate It blocks per assertion.
[MINOR] internal/books/metadata_service_test.go:459 — "no series fallback" test mixes setup/invocation/assertions in one It
The Describe("RefetchMetadata comic book with no series fallback") block constructs the refetch function, calls it, and asserts three things (err, Title, Series) all inside a single It. This violates the BeforeEach/JustBeforeEach separation and the one-Expect-per-It rule. Fix: refactor to standard BeforeEach/JustBeforeEach/It structure with individual assertion Its.
[MINOR] internal/books/metadata_service_test.go:368 — Standalone It("returns no error") on the comic happy path should be folded
Line 368 has a bare It("returns no error") { Expect(err).NotTo(HaveOccurred()) } in the happy-path Describe. Convention says to fold the nil-error check into the value assertion (Expect(result, err).To(...)) rather than a standalone block. The bool result from RefetchMetadata (didUpdate) gives a natural fold target: Expect(didUpdate, err).To(BeTrue()).
Security surface — all clean:
REVIEW VERDICT: 0 blocker, 0 major, 3 minor
CODE REVIEW: NOT APPROVED
Phase 0: DEMO Verification
No DEMO block found in
bd show bookshelf-vnu3orbd comments bookshelf-vnu3. For a background workflow fix a live DEMO is acknowledged as impractical (requires full docker stack + populated DB + workflow trigger). Proceeded on the strength of the unit test coverage and green CI, but flagging the absence per review standard.Phase 1: Spec Compliance
All five spec requirements are present in the diff (is_comic detection, comic MatchQuery, Metron wiring, persist routing, tests). However, requirement 3 has a correctness gap — see MAJOR below.
Phase 2: Code Quality
[MAJOR] internal/books/metadata_service.go:887 — Metron is called for every non-comic (ebook/audiobook) book in bulk enrich
The provider-capability filter is one-directional:
When
isComicQuery == false(ebooks, audiobooks), the&&short-circuits and the guard never fires. Metron (capabilities["comic-issue", "series"]) passes through intoactiveProvidersfor every ebook. For a 250 k-book library with 10% comics, each bulk enrich sweep sends ~225 k needless requests to Metron. Those requests burn themetronBulkIntervalrate-limit budget that was explicitly set to leave headroom for interactive fetches, defeating the intent. Metron will return ErrNoMatch (so no data is corrupted), but the rate-limit drain is real.Fix: add the symmetric guard — skip comic-only providers for ebook queries:
(The existing capability vocabulary already supports this: ebook providers carry
isbn/title/author; Metron carries onlycomic-issue/series.)[MINOR] internal/books/metadata_service.go:248 —
stripFileExtduplicates existing extension-stripping logicprimaryComicFileStemincomic_search_prefill.go:74already strips the extension usingstrings.TrimSuffix(f.FileName, filepath.Ext(f.FileName)). The new hand-rolled loop (stripFileExt) does the same thing with an extra/boundary guard that is unused in practice (DBfile_nameis a bare filename, not a path). Extract and reusefilepath.Ext+strings.TrimSuffix, or expose a shared helper, to avoid two implementations diverging.[MINOR] internal/books/metadata_service_test.go:~1718 — last Describe violates one-Expect-per-It and BeforeEach/JustBeforeEach/It separation
Describe("RefetchMetadata comic book with no series fallback")puts setup, invocation (_, err = refetch(ctx, 1, nil, nil)), and threeExpectcalls all inside a singleItblock. Per project conventions, setup belongs inBeforeEach, the call under test inJustBeforeEach, and eachItholds exactly oneExpect. The other new test blocks follow the pattern correctly; this one regresses.REVIEW VERDICT: 0 blocker, 1 major, 2 minor
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)