From bd24ecbafd65287a337fe453cc79a07cce1f5632 Mon Sep 17 00:00:00 2001 From: Sprite Date: Wed, 9 Sep 2026 10:13:36 +0000 Subject: [PATCH] docs: first on-device test results and metadata-source research MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the coil3 StateFlow trap (a Kotlin warning, not an error, that silently blanked every book cover) so a future session greps for it, and the two Open Library cover defects behind it. METADATA-SOURCES.md answers the user's research question about alternative lookup sources: measurements over a 60-ISBN sample drawn from a third-party catalogue, the options with costs, and a recommendation to fix diagnosis before buying coverage. Nothing there is implemented — the user asked to be consulted first. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf --- docs/HANDOFF.md | 49 ++++++++++++ docs/METADATA-SOURCES.md | 166 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 docs/METADATA-SOURCES.md diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 4732d75..fa36f16 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -285,3 +285,52 @@ library, scaffold — each light + dark). survive minification. 4. Still-open user questions, unchanged: where the server will actually live, and the two account emails for `create-user.sh`. + +## First on-device test — 2026-09-09 +The user installed the signed APK on a real phone. It runs. This closes the +"never been run" gap that waves 1-4 all carried. Six issues came back; five were +fixed directly by the orchestrator in commit `356f639` (they were small, and +spinning up Sonnet workers for two-line Compose edits costs more than it saves). + +**The one worth remembering** — `BookCover` branched on `painter.state`, but in +coil3 that is a `StateFlow`, not a `State`. Every `is +AsyncImagePainter.State.X` arm was therefore always false, and because a `when` +used as a statement needs no `else`, it compiled clean and drew NOTHING — no +cover, no placeholder, no error icon. Kotlin emitted "Check for instance is +always 'false'" as a *warning* on four consecutive lines and the build stayed +green. **Grep the build log for `always 'false'` before accepting a wave**; that +warning class is a silent-dead-code detector and this build had it for months. + +Two more cover defects sat behind it, both verified against the live service: +- `covers.openlibrary.org/b/isbn/{isbn}-L.jpg` answers **200 with a 43-byte 1x1 + transparent GIF** for an edition with no art. Any image loader calls that a + successful load. Only `?default=false` turns a miss into a 404. +- OL's DTO synthesized that URL unconditionally, so `MetadataMerger`'s + fill-blanks rule could never reach Google Books' thumbnail. The SPEC'd cover + fallback was dead code. Cover URLs now come from OL's own `cover` object. + +Also fixed: sync bar clipped by rounded display corners (now owns its +navigation-bar inset, wider horizontal padding, moved into Scaffold's `bottomBar` +slot); setup screen's Password field hidden behind the IME (`safeDrawingPadding` +outside `verticalScroll`, plus Next/Next/Done IME actions and +`windowSoftInputMode=adjustResize`); library card titles reflowed (20sp leading, +author gets its own 4dp gap); scan sheet now names the ISBN and says +"Searching…" instead of showing a bare spinner. + +| Check | Result | +|---|---| +| `./tasks/gw assembleDebug` | exit 0 | +| `./tasks/gw testDebugUnitTest` | exit 0 — 106 tests, 1 skipped, 0 failures | +| `./tasks/gw verifyPaparazziDebug` | exit 0 against re-recorded snapshots | +| `./tasks/gw assembleRelease` | exit 0 — 41,777,376 bytes | +| `apksigner verify` | V2 signer `CN=Bookshelf, O=Montanaro` — real release key | + +### Open, not started: metadata coverage +The user reported 1 of 3 scans resolving, and asked for **research, not a +change**. Findings are in `docs/METADATA-SOURCES.md`. Headline: Open Library +answered 88% of a 60-ISBN sample, keyless Google Books returned **429 on 60 of +60** requests, and both clients collapse every non-200 into `null` — so a +rate-limited lookup reaches the user as "No match found." Recommended order is a +free Google Books API key, then distinguishing "couldn't ask" from "not found", +then retry/backoff, before adding any new source. **Awaiting the user's decision; +do not implement unasked.** diff --git a/docs/METADATA-SOURCES.md b/docs/METADATA-SOURCES.md new file mode 100644 index 0000000..243a150 --- /dev/null +++ b/docs/METADATA-SOURCES.md @@ -0,0 +1,166 @@ +# Book metadata lookup — why scans miss, and what else we could ask + +Research note, 2026-09-09. Written in response to "out of the 3 barcodes I've +scanned, only 1 has been discovered properly." + +**Nothing in here has been implemented.** SPEC's two-source design (Open Library +primary, Google Books fallback) is unchanged. This is the evidence for deciding +whether to change it. + +--- + +## Short version + +Three separate defects were making lookups *look* far worse than the underlying +data actually is, and all three are now fixed (commit `356f639`). They are not +the same problem as "this book isn't in the database": + +1. A cover that loaded fine still rendered as nothing, so a **successful** lookup + looked like a failed one. That alone could account for the book you did find + appearing broken. +2. Open Library's cover URL was synthesized for every book whether or not art + existed, and a missing cover comes back as a **200 with a 43-byte 1×1 + transparent GIF** — a "successful" load that paints nothing. +3. Because that synthesized URL was never blank, the merge rule could never fall + through to Google Books' thumbnail. The documented fallback was dead code for + covers. + +What is left is a real coverage question, and there the measurements point at one +thing above all others: **the Google Books fallback is probably not answering at +all.** Every keyless request from this machine returned HTTP 429, and the app +turns any non-200 into `null`, which the UI presents as "No match found — enter +the details by hand." A rate-limited lookup and a book that genuinely exists +nowhere are, right now, indistinguishable to both you and me. + +My recommendation is to fix the diagnosis before buying more data. Details in +[Recommendation](#recommendation). + +--- + +## What I measured + +**Sample.** 60 ISBNs drawn from the Harvard Library catalog — deliberately a +third party, so the sample doesn't presuppose the answer by coming from one of +the two sources under test. Ten publishers, weighted toward the small Catholic +and homeschool presses that a MODG family's shelf actually carries (Ignatius, +TAN, Sophia Institute, Bethlehem Books, Baronius) alongside mainstream trade +(Penguin, Random House, Scholastic, Crossway, Loyola). + +**Method.** Direct HTTP against each API, one ISBN at a time, 1.2 s apart. A +source "hits" only if it returns a usable title. + +### Results + +| Source | Hit | Miss | Error | Hit rate | +|---|---|---|---|---| +| Open Library Books API (what the app calls today) | 53 | 4 | 3 | **88%** | +| Google Books, keyless (the app's fallback) | 0 | 0 | **60 × HTTP 429** | **0%** | +| Harvard LibraryCloud | 56 | 4 | 0 | 93% * | +| Open Library cover art exists for the ISBN | 47 | 13 | 0 | 78% | + +\* Harvard is where the sample came from, so its number is inflated by +construction. It's here to show the API works and answers by ISBN-13, not as a +fair comparison. + +Two further observations from the same runs: + +- **Concurrency is punished.** The same 60 ISBNs run six-at-a-time dropped Open + Library from 88% to 70%, entirely through transport errors. The app makes one + request per scan, so this doesn't bite in normal use — but it does mean + "Open Library missed" in a log is not proof the book is absent. By the end of + this research my own IP was refused outright for a while. +- **17% of successful Open Library lookups have no cover art at all** (9 of 53). + Even with everything working, roughly one book in six will legitimately show + the placeholder. That is a data fact, not a bug, and it's worth knowing before + you read a placeholder as a failure. + +### What this does not tell us + +Worth saying plainly, because it bounds how much weight the numbers carry: + +- n = 60, and the sample comes from a research library. It under-represents + recent mass-market paperbacks, reprints and print-on-demand editions — which + is exactly where Open Library is thinnest. Real shelf coverage is probably + *below* 88%. +- Every request came from a datacenter IP. The Google Books 429 may partly be + this host sharing a quota pool with other tenants; **your phone, on a + residential or mobile IP, may well get answers.** That's precisely why the + app needs to be able to tell us which it got. +- I don't know which three ISBNs you scanned. If you still have the books to + hand, those three numbers are worth more than another 60 sampled ones. + +--- + +## The options + +### A. Give Google Books an API key +Free, 1,000 requests/day, no billing account required. Turns the fallback from +"silently 429" into a working source. Roughly a dozen lines: a key in +`local.properties` → `BuildConfig` → `&key=` on the query. + +The key ships inside the APK and can be extracted, so restrict it to the Books +API in the Google Cloud console. At our volume, someone stealing it costs us +nothing but the quota. + +**Effort: hours. Cost: free. Likely the single biggest win.** + +### B. Tell the difference between "not found" and "couldn't ask" +Both clients collapse every non-200, timeout and parse failure into `null`, and +`MetadataRepository` collapses that into "no match", and the UI writes "No match +found." A book that's offline, rate-limited, or hit a 500 is reported to you as +a book that does not exist. + +Distinguishing these gets you a retry button instead of a manual-entry form, and +gets me a real answer next time you say "it missed." + +**Effort: half a day. Cost: free. Do this regardless of what else we choose.** + +### C. Retry with backoff +One retry on 429/5xx, a couple of seconds apart. Standing at a bookshelf, a +two-second retry is invisible; a manual-entry form is not. + +**Effort: an hour. Cost: free.** + +### D. Add a third free source +Only worth doing after A–C, when we can see what's actually still missing. +Ranked by what I'd try first: + +| Source | Key? | Notes | +|---|---|---| +| **Open Library `search.json`** | No | Searches the whole OL index rather than the edition table the Books API reads. Cheapest possible fallback — same service, one more request, no new failure modes. | +| **Harvard LibraryCloud** | No | Free, no registration, answers by ISBN-13, verified working. Strong on older, scholarly and small-press books — the shape of gap we'd expect. Returns MODS; no cover art, and no ISBN-13 for pre-EAN books unless we convert. | +| **Library of Congress** | No | The SRU endpoint (port 210) is blocked from here; the `loc.gov` JSON API responds. Excellent for US imprints. Needs more probing before I'd commit. | +| **K10plus SRU** | No | Free German-led union catalogue, large and international. Cataloguing conventions differ enough that merging would need care. | + +Dead ends, so nobody re-investigates them: **OCLC Classify** (retired 2021), +**Goodreads API** (retired 2020), **Amazon Product Advertising API** (requires an +affiliate account with qualifying sales), **WorldCat Search** (requires OCLC +membership — institutional pricing). + +### E. Pay for ISBNdb +~$15–50/month depending on tier. Genuinely better coverage than anything free, +including cover art, and a single clean API. It is also a subscription for a +two-person home library, and I'd want proof that A–D leave a real gap before +recommending it. + +**Effort: hours. Cost: $180–600/year.** + +--- + +## Recommendation + +Do **A + B + C** together — they're cheap, they're independent of any decision +about new sources, and between them they cover the most likely explanation for +1-in-3. Then rescan the same books and let the app tell us what it actually got. + +If a real gap survives that, add **Open Library `search.json`** (D) as an +in-family fallback before reaching for a new organisation's API, and Harvard +after that. + +I'd hold off on **E** entirely until we have numbers from your own shelf. Paying +for coverage we might already have would be the wrong order. + +One thing worth deciding separately: 17% of books legitimately have no cover art +anywhere. The placeholder now looks deliberate rather than broken, but if you +want covers on everything, that's a different feature — photograph the book, +store it as the cover — and not a metadata-source problem at all.