The v1 zero-listing guard (f6e9727) was bypassed by a partial degraded
mount: if even one song was visible, current_files was non-empty and
delete_missing ran freely, pruning every invisible DB row (Creed r1 HIGH).
Guard contract (god ruling):
- Auto scans (startup, periodic, /api/rescan): refuse when
would_remove >= max(_PRUNE_MAX_ABS=1, _PRUNE_MAX_FRAC=0.5 * existing).
Zero-listing also refused (would_remove == existing).
Scan sets stage='error', leaves DB intact.
- /api/rescan/full: allow_mass_prune=True → guard logs a warning and
proceeds; delete_missing runs (user-authorised explicit intent).
Changes:
- lib/scan.py: _PRUNE_MAX_ABS/FRAC constants; combined guard (zero +
partial) before delete_missing; allow_mass_prune param on
background_scan(); _scan_mass_prune_next global threaded through
kick_scan() and _scan_runner()
- server.py: trigger_full_rescan calls kick_scan(allow_mass_prune=True)
- tests/test_scan_prune_guard.py: Case B (Creed partial, RED f6e9727→
GREEN here) + Case C (full-rescan bypass)
Gates: pytest 2816/2816, JS 1155/1155
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
When background_scan() discovers zero songs in the DLC directory but the
songs table is non-empty, skip delete_missing and return with stage='error'.
An empty listing on a non-empty library almost certainly means the DLC mount
was temporarily inaccessible (FUSE remount, NTFS dirty-flag RO fallback, brief
unmount mid-scan) rather than every song being genuinely deleted.
delete_missing({}) on a non-empty DB deleted ALL rows — the #P1-libpurge
incident that wiped 50,943 songs was caused by exactly this path.
The guard fires only when current_files is empty AND the DB has at least one
row, so a genuinely empty new library is unaffected.
Test: test_scan_prune_guard.py::test_empty_listing_refuses_prune_when_db_nonempty
Failing input: dlc dir with no feedpak/sloppak/wem + 1 DB row.
Before: delete_missing({}) fires, row gone.
After: stage='error', row survives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj