51e3d614b2
`backend_consistency ?deep=true` re-reads every chunk and re-hashes it to catch silent bit-rot, and records `blob_corrupted` (severity `data_loss`) naming `backend.backend_type()`. It was reading through the live backend — which for a remote backend includes `CachedBlobBackend`, whose `get_blob_stream` returns the local cached file and never touches the remote on a hit. So the attribution was false in both directions: rot on S3 hidden by a good cached copy, and rot in the cache reported against a healthy S3 — the second sending an operator to the wrong layer entirely. Surfaced by a real run: 2022 chunks, 321 ms shallow, 1.5 s deep. That is 0.74 ms per chunk for a full read plus BLAKE3, sequential, over S3 — impossible, and explained by every chunk being cache-warm. A genuine uncached sweep is tens of seconds. Adds `BlobStorageBackend::uncached()`, defaulting to `None`. `CachedBlobBackend` returns its inner; `Retry` and `Swappable` forward so the unwrap reaches the cache through them. `Swappable` resolves via `current()` rather than capturing a handle, because it sits OUTSIDE the cache — a DI-time snapshot would keep pointing at pre-cutover storage and audit the backend a migration just moved away from. Only the cache is peeled. The cache stores plaintext and the content hash is over plaintext, so unwrapping past the encryption decorator would hand back ciphertext and fail every blob it checked. **No change to normal reads.** `uncached()` is called in exactly one place, and the unwrapped handle is used at exactly one call site (`verify_bytes`). Enumeration, every other job, and every request path still go through the cached stack. `?storage=<entry>` was already correct — `build_entry_backend` has no cache decorator — so this only fixes the live-backend path, which is the one that was silently fast. Also reports `verified` in the run extras, on every run including zero. A deep run that verified nothing and one that verified everything were otherwise indistinguishable in the outcome, which is what made a 1.5 s "deep" sweep look plausible in the first place. Same lesson as `orphans_covered` on the Azure fallback: a check that cannot report its own coverage will eventually be believed when it should not be. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>