fa120081a1
Opening a book via Android "Open with" on a cold start could clear the entire library. `openTransient` built an ephemeral entry on top of the not-yet-loaded (empty) store; the library page's `length > 0` cached-skip then skipped loading the real library from disk; and a later `saveLibraryBooks` persisted the empty/partial set — overwriting both library.json and its .bak. Introduced by #4407 (transient "Open with"), made reliably reproducible by #4527 (reliable cold-start delivery) — so released v0.11.4, which lacks #4527, does not reproduce it. Two layers of defense: - saveLibraryBooks now merges with the on-disk library (union by hash, incoming wins), so a routine save is monotonic: it can add or modify rows (including `deletedAt` tombstones) but can never drop a book. Deliberate, authoritative rewrites (restore, tombstone GC, account reset) opt in via the new `{ replace: true }`. This layer alone makes the wipe impossible. - openTransient loads the real library from disk before importing a transient book — also fixing the cold-start hash-match miss that re-imported already-imported books — and the library page's load-skip now gates on the store's `libraryLoaded` flag instead of `length > 0`. Tests cover the merge floor (no-drop / no-wipe-on-empty / tombstone preserved / incoming-wins) and both `{ replace: true }` paths. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>