808b792a02
`invalidate_subtree` had exactly one caller, the watcher. No `rm -r`, recursive prefix delete or `rename` reached it in either language, so every listing and body cached *below* a deleted or moved directory kept being served: `ls` printed a deleted directory's contents and `cat` returned a deleted file's bytes, both exit 0, until the index TTL expired. Reproduced against live minio with the bucket verified empty, so the reads were provably cache rather than survival. Each mutation site reported `invalidate_after_unlink`, which evicts the path's own listing and its parent's -- complete for a file, blind for a subtree, because everything beneath was cached under its own key and nothing above it evicts one. `cache/context` gains an `invalidate_subtree` beside the two it already had, and `CacheInvalidator` gains the method. It delegates to the manager rather than walking: ancestors can be assembled from `invalidate_after_write` calls because the caller knows the chain, but only the caches know which keys lie beneath a path. Then one rule in both languages -- an op that destroys or moves a whole subtree evicts that subtree -- so every `rm_r`/`remove_prefix` and both endpoints of every `rename` swap over. It is a strict superset of `invalidate_after_unlink`, so a file path behaves exactly as before. `unlink`, non-recursive `rmdir` and `copy` are deliberately untouched. This came out of the kit plan's "delete-side `invalidate_ancestors` asymmetry sweep", whose premise was wrong: `invalidate_ancestors` exists because one op can materialize several levels at once, which no delete does on a real-directory backend, and the keyed-store delete that does need it already had it. box and gdrive only looked like they were missing it -- their `mkdir -p` invalidates per created level in a bespoke loop. Also corrects two `emulate_truncate` docstrings that still named s3/ssh/ram/redis as the emulating backends; #858 left dropbox as the only one. Pinned by `integ/resources/cache/subtree_evict.json`, 16 cases across s3/s3-prefix/gridfs/gridfs-prefix, captured empirically on both hosts and checked falsifiable by reverting the object_store half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>