Решили: оба порога «Изморози» судят по снимку начала тика (ctx.Stacks), а не по живому числу (ctx.Effect.Stacks); запрет на прямую форму живого чтения в компонентах эффектов держит гейт EffectStacksVisibilityGateTests.

Почему: это второй раз, когда открывается ровно эта дыра. Первый — «Угли», развели зеркало на тике 300 и породили сам закон видимости; тогда починили один компонент и записали правило в EffectContext.Stacks. «Изморозь» несла ту же ошибку в обоих порогах и не была покрыта ни одним тестом. Отвергнутая альтернатива — «починить и здесь, оставив правило комментарием»: комментарий стоит в EffectContext, а нарушают его в чужом файле, где ctx.Effect лежит под рукой и выглядит тем же самым числом. Инвариант кросс-файловый, значит его место в тесте.

Гейт намеренно ловит только прямую форму .Effect.Stacks. Обращение через локальную (var eff = ctx.Effect; eff.RemoveStacks(1); if (eff.Stacks <= 0)) — это управление сходом стаков, и там живое число единственно верное. Полного статического анализа у нас нет, и заводить его ради этого правила дороже, чем оно стоит: наивный путь закрыт, а изощрённый в этот класс ошибок не приводит.

Грабли: дыру нашла панель аудита, а не тест и не игра, — потому что видимого симптома у неё нет. Разойтись могли только два клиента коопа или два прогона зеркала, а зеркальные тесты «Изморозь» не гоняют: у неё вообще не было тестов. Сценарий отказа при этом бытовой — цель на девяти стаках, десятый кладут в тике N, и ледяные удары этого же тика уже усилены; величина зависит от круга ResolveCombatRounds, в котором разрешён удар.

Владелец правды: Combat/Effects/Components/FrostComponent.cs, Combat/Effects/EffectContext.cs; тесты FrostTests, EffectStacksVisibilityGateTests.