The user supplied the failing ISBNs: 9781883937386 (The Hittite Warrior)
and 9781883937676 (Shadow Hawk), both Bethlehem Books. Both resolve
against the source the app already queries — title, author, publisher,
page count, cover art — and both real responses parse correctly through
the app's own OpenLibraryClient. Captured as fixtures with a regression
test, because these are the specific books that motivated the research.
That kills the coverage hypothesis for these two and demotes Harvard,
which holds neither: its 93% in the sample table is inflated by
construction (the sample was drawn from Harvard) and misleading in
exactly the direction that matters — it is a research library and does
not carry small-press children's historical fiction.
The failure is upstream of the metadata sources. Documented the three
candidates; the leading one is that the barcode never decoded into a
valid ISBN-13, which ScanCodeFilter drops silently with no UI feedback
whatsoever. Revised the recommendation accordingly: make the app say
what happened before adding any source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf
The cover one is the interesting bug. BookCover branched on
`painter.state`, but in coil3 that is a StateFlow<State>, not a State —
so every `is AsyncImagePainter.State.X` arm was always false. A `when`
used as a statement needs no else, so it compiled clean and drew
NOTHING: no cover, no placeholder, no error icon. That is why a
successfully looked-up book showed as a bare title floating in space.
Collect the flow before matching on it.
Two more cover defects sat behind that one:
- OpenLibraryDtos synthesized covers.openlibrary.org/b/isbn/{isbn}-L.jpg
unconditionally. For an edition with no art that URL answers 200 with a
43-byte 1x1 transparent GIF (verified against the live service), which
an image loader calls a successful load. So even with the state bug
fixed it would have painted an invisible cover and never fallen back.
Now the URL comes from OL's own `cover` object, which is present only
when art actually exists.
- Because that URL was never blank, MetadataMerger's "fill blanks from
the other source" rule could never reach Google Books' thumbnail. With
OL reporting null, the fallback works, and MetadataRepository adds the
by-ISBN URL as a genuine last resort — with `default=false`, so a miss
is a 404 the loader can report instead of a blank image.
The placeholder itself is now drawn in every non-success state (loading
included, where it previously drew an empty box) and reads as a book:
mahogany spine strip, gold hairline, letterpress panel.
Also:
- SyncStatusBar owns its navigation-bar inset and takes wider horizontal
padding, so it clears a phone's rounded display corners; it moves into
Scaffold's bottomBar slot so its height reaches the content padding.
- SetupScreen takes safeDrawingPadding outside verticalScroll, so the IME
shrinks the viewport instead of covering Password, plus Next/Next/Done
IME actions.
- Library card titles drop to a 20sp line height with the author given
its own 4dp gap: at titleSmall's 24sp leading a wrapped title left the
author closer to the last title line than the title lines were to each
other, so the byline read as part of the title.
- The scan sheet now names the ISBN it just read and says "Searching…",
so a decoded barcode is legible as decoded and the user can lower the
book instead of holding it to the camera at a bare spinner.
assembleDebug + assembleRelease exit 0; 106 unit tests, 0 failures;
verifyPaparazziDebug green against re-recorded snapshots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf
Wave 4 is verified complete. Also records the two mechanisms that stopped F3
from reporting (the 600s background-task ceiling in `claude -p`, then the quota
wait) so the next worker launch does not repeat it, and flags the one thing
wave 4 owed and never delivered: nobody has looked at the fourteen new
screenshots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TyzeWmdTqi7U85iYNGy7P
setsid nohup did not survive orchestrator teardown on 09-06 (uptime was
continuous, so this was a teardown kill, not the hazard-#5 suspend).
tasks/service-worker.sh runs a worker under the sprite service supervisor
instead, which both outlives the orchestrator and holds the box awake, making
the task-lease guard redundant. It is sentinel-guarded so a supervisor restart
does not re-run a finished wave, and it stops its own service afterwards so the
sprite can suspend.
logs/WAVE4-DONE records that F3's files were all complete but the worker was
stuck re-running verification it could not finish, so the orchestrator ran the
verification itself and accepted the work. F3's own written report — including
the design critique of the rendered screens it was asked for — was never
produced; that gap is recorded in the sentinel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TyzeWmdTqi7U85iYNGy7P
Completes wave 4. Verified by the orchestrator, not self-reported:
assembleDebug / testDebugUnitTest / assembleRelease all exit 0;
102 tests, 1 skipped, 0 failures, 0 errors.
- Settings showed the PocketBase user id instead of the signed-in email,
because login never persisted the email. AuthRepository now writes it to
SettingsStore on success and sign-out clears it; SettingsUiState carries
userEmail in place of userId. AuthRepositoryTest asserts both directions.
- Paparazzi coverage for the five screens library was missing: setup, detail,
scan, locations, settings, each light + dark, populated rather than empty.
Scan cannot show a live camera under Paparazzi, so its tests render the
reticle overlay and the result bottom sheet over a static backdrop.
- Release signing via an optional gitignored app/keystore.properties. Without
it assembleRelease still works and comes out debug-signed, so the build is
not owner-only. R8 deliberately left off; nothing has proven Room, Retrofit,
kotlinx-serialization and ML Kit survive it.
- Top-level README: shared-library model, offline-first architecture, the
push-then-pull last-write-wins conflict rule SPEC requires be documented
here, build/deploy/install steps, and honest current limitations.
The signed APK and the keystore are intentionally not committed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TyzeWmdTqi7U85iYNGy7P