Решили: отсутствие UnitVisual или ViewPrefab у юнита стало ошибкой валидации контента — показывает Doctor в Content Hub, роняет CI тест UnitVisualPresenceTests, который гоняет по всему ростеру то же правило из ContentValidationService.ValidateUnitVisual.

Почему: отвергли гейт в OnValidate у самого SO. Юнита проектируют раньше, чем ему рисуют тело, и запрет на сохранение заставил бы подсовывать заглушку ради обхода — то есть врать данным ровно там, где мы их и проверяем. Пара «панель + тест» этого не требует: автору видно сразу, а мимо CI не проедет.

Рантайм-эррор в UnitView при этом НЕ убран, хотя гейт делает его почти недостижимым: он ловит второй случай — клип атаки без маркера контакта, — которого проверка при авторинге не видит, и юнитов, собранных не из ассета.

Оба тултипа обещали молчаливый фолбэк («пусто = дефолтный из презентера», «null = статичный фолбэк»), и это обещание снято тем же диффом: именно оно прятало поломку, выпуская юнита на арену чужим телом. Внутри нашего кода фолбэк — баг разводки, а не поведение.

Сказано: «Надо гейты или ошибки на юните создать. А то когда визуала не было и я пытался ставить юнита - мне был еррор лог, а он ведь должен быть еще при создании SO, чтобы такое не повторилось!» (2026-08-03, сессия af8ef6b9). Место гейта выбрано инженерно, порядок работ — вердикт Макса: близнецов не разводим, гейт только про отсутствие тела.

Грабли: сторож по соседнему инварианту уже существовал и лежал КРАСНЫМ. UnitTintPolicyTests проверяет, что юниты, делящие спрайт, различаются ступенью BodyShade, и падает на двух группах (шесть юнитов на арте Goblin Fighter, из них трое без приглушения; TrashHerbalist и LightShepherd с одинаковым Ash). Нарушения старше 03.08.2026 и в этом заходе не чинились — осознанно, вердиктом Макса. Отсюда вывод на будущее: перед тем как заводить новый гейт, прогони соседние — красный сторож не отличим от отсутствующего, а мы чуть не завели второй поверх незамеченного первого.

Второе, мелкое: правило вынесено чистой функцией от UnitData (по образцу ValidateIdString), а не методом от ContentEntry. Иначе тест не смог бы позвать его без построенного индекса и повторил бы логику у себя — то есть у инварианта стало бы два владельца.

Владелец правды: ContentValidationService.ValidateUnitVisual, тесты UnitVisualPresenceTests (весь ростер) и ContentValidationServiceTests.UnitWithoutBody_IsTwoIssues (сторож самого сторожа).