StatResolver's _cache, _modifiers and _generations were keyed by owner and
nothing ever removed an entry, contradicting Sec 0b's promise that a resolver
entry is "destroyed at unequip / entity teardown." A ConsumableItemEffect that
expired still had a generations entry, and an unequipped EquippedItem's entries
lived on for the rest of the process -- the same shape of leak the catalog
object had before Cut 2, one level down.
Forget(owner) drops every entry for that owner across all three dictionaries;
Entity.TryUnequip and the consumable-expiry branch in Entity.Update now call it.
Also adds GenerationOwnerCount/CacheEntryCount/ModifierEntryCount, a test-only
surface a mutation test pins the retained-set claim against.
F3: InvalidateSource's comment claimed a stat "recomputes only when a term's
source changes." Entity.cs's UpdatePerformance calls it unconditionally every
tick for Heat and Durability, regardless of whether the value moved, so the
true invariant is "at most once per tick per (item, stat)." Corrected, and
noted Quality as the one source this scheme still meaningfully defends
against a real no-op tick (see F6 in ItemData.cs).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>