Статус: Living — живой реестр, правится при закрытии пункта.


Сознательно отложенный технический долг и главная будущая таска. Живой документ — в отличие от Meta - Tech Changelog (архив историй) и папки journal/ (записи о принятых решениях, append-only).

Выделено из tech-changelog 2026-07-30 при разделении журнала: история и открытый долг — разной природы, у первой нет владельца-в-коде, у второго есть срок жизни.

Легенда: значит «баг существует», а не «исправлен»

Пункт со статусом ✅ подтверждён по коду — это живой дефект; исправленные помечены словом ЗАКРЫТО с датой сверки. Перед починкой пункта всё равно сверяйся с живым кодом: реестр отстаёт по устройству, и это его нормальное состояние, а не поломка.

Ресверка §3.8 сделана 07.08.2026 (панель аудита): C1, H1, H4, H6 закрыты, H10 неверен на текущем дереве, C2–C6 и H2/H3/H5/H7/H8/H9 живы. Сетевые пункты не сверялись — кооп в работе.

Нумерация разделов (§3, §4) сохранена от исходного tech-changelog — на подпункты ссылаются внешние аудиты (docs/audits/) и якоря внутри этого файла. Переименование порвало бы ссылки без выигрыша.


3. Открытый техдолг (сознательно отложено)

Эти пункты не баги прямо сейчас — это запланированный долг. Чинить, когда дойдёшь до соответствующей фазы, с тестом на руках.

3.0 ЗАКРЫТО 30.07: зеркало расходилось на хиле Друида

MirrorMatchTests.Mirror_SquadSeries_NeverDiverges(5) падал детерминированно на тике 543: у левого Defender висел effect.spore_mend, у правого нет. Причина — чтение живого списка эффектов вместо состояния начала тика: «Взрыв спор» считал уникальные яды на цели своим обходом ActiveEffects, а чужой клинз, прошедший раньше в том же тике, яд уже снимал. Левый Друид успевал детонировать, правый бил в пустоту — исход решало место юнита в обходе.

Починено тем же приёмом, что уже стоял в диспеле: счёт судит по началу тика (EffectSystem.CountUniqueTaggedAtTickStart) — эффекты, легшие раньше этого тика, плюс снятые в этом тике. Владелец правила один, локальная копия в AbilitySystem удалена. Держит PoisonBurnThornsSliceTests.UniqueTagCount_CountsWhatWasThereAtTickStart.

Разбор — запись журнала «A Cleanse Must Not Rob A Detonation Of The Same Tick» (2026-07-30).

3.1 ⑩ Разбухание CombatSimulation (god-object)

CombatSimulation совмещает: тик-цикл, реализацию ICombatContext, очередь команд, хаб C#-событий, фабрику снарядов, расчёт checksum (~340+ строк). Пока читаемо, но это место будет пухнуть. Когда дойдёт (а не сейчас — иначе преждевременная абстракция): вынести очередь команд и checksum в отдельные классы. Приоритет — низкий, до появления реальной боли.

3.2 Аллокации в Stats.RebuildCache

Пять массивов на каждую пересборку кэша статов: четыре new float[StatCount] (flat, percentAdd, multAccum, overrideVal) и один new bool[StatCount] (hasOverride) — Combat/Stats/Stats.cs:210-214. (Сверено 07.08.2026; раньше здесь стояло «три» — число отстало от кода, каскад с тех пор оброс Override-группами.) В Фазе 1 пересборка одноразовая (модификаторы ставятся раз при создании юнита) — неважно. В Фазе 2 эффекты будут часто менять статы → пересборки участятся → перевести на переиспользуемые инстанс-буферы. Триггер: подключение стат-меняющих эффектов в контент.

3.3 Грубый alpha интерполяции презентации

CombatPresenter.Update берёт alpha = Time.deltaTime / TickDelta — это не корректная доля интерполяции (правильная — accumulator / TickDelta из CombatLoopService). Движение сглажено, но не идеально по фазе. Фикс: пробросить дробную часть аккумулятора из loop в презентер. Косметика, не срочно.

3.4 Таргетинг O(n²)

Выбор цели идёт полным перебором. На десятках юнитов это дешевле запроса к SpatialHash — осознанно. Когда число юнитов вырастет (Фаза 3) — перевести на SpatialHash.QueryRadius.

Сверено 07.08.2026: пункт называл владельцем TargetingSystem, а такого класса в дереве нет с Фазы 3 — его «поглотил» BrainSystem, и живой перебор сидит в ProfileBrain. TODO в коде, на который ссылался пункт, тоже не нашёлся. Строка X3 ниже сообщала об этом же расхождении и потому снята: реестр не должен спорить сам с собой.

3.5 Валидация контента эффектов

Правка ⑤ убрала утечку мгновенных эффектов рантайм-способом (OnApply→OnExpire). Но лучше бы ещё запрещать на этапе авторинга ставить stateful-компоненты на мгновенный эффект (валидация в EffectData/редакторе). Аналогично — предупреждать о бессмысленных комбинациях. Задача редакторного тулинга (Фаза 4, Odin-авторинг).

3.6 Вампиризм применяется ко всему урону

Правка ② вешает lifesteal на любой нанесённый урон (включая DoT/шипы), т.к. у урона пока нет тегов источника. Сузить до авто-атак/способностей — когда появятся теги источника урона (Фаза 2).

3.7 Примирить LifestealComponent под моделью B

Реактивный LifestealComponent лечит напрямую, минуя стат — под моделью «эффекты кормят статы» он избыточен и создаёт риск двойного учёта (стат-путь + компонент на одном юните). Пока только помечен баннером (на нём держится ReactiveEffectTests). Когда начнётся авторинг контента: удалить компонент, заменить вампирик-баффы на StatModifierComponent(+Lifesteal), мигрировать тест (оставить в нём ThornsComponent как пример реактивности). Триггер: первый реальный контент с вампиризмом.

3.9 Атака из нескольких Ударов — СДЕЛАНО 2026-07-30/31, открыты четыре следствия

Реализовано (коммит 858cd5413, журнал «An Attack May Be Several Hits»): клип размечается несколькими маркерами Hit, AttackTiming.ContactTicks раскладывает их по тикам свинга, AutoAttackSystem.Resolve разрешает каждый контакт своим расчётом урона и своими on-hit, доля силы каждого Удара задаётся в UnitData._hitDamageShares. Рефанд кулдауна получает только пустой свинг. Первый носитель — Монах, два маркера с долями 0.5 / 0.5.

Раздел оставлен ради вердиктов и открытых следствий ниже; описание «как зашито было» снято, чтобы не читалось как текущее состояние.

Вердикты Макса (2026-07-30), четыре из четырёх:

  1. Досягаемость проверяется один раз на свинг — так и оставляем. Промах по убежавшей цели это не дефект, а событие: Evade, и оно должно явно писаться (боевая цифра / лента, как остальные исходы). Дизайнерски это подарок: кайт становится видимой контригрой против длинных серий.
  2. Контроль прерывает анимацию: сделал один удар из двух — второй не доигрывает. Требование при этом жёсткое: микростаны не должны рекастить авто-атаку и тем ускорять противника. Решение — ниже.
  3. Баланс — отдельным заходом, когда тему будут реализовывать в тех-плане. Здесь только зафиксировано.
  4. Детерминизм — целочисленные тики, подтверждено.

Решение по (2): якорь интервала — ПЕРВЫЙ контакт свинга.

Механизм ускорения существует и найден в коде: Interrupt делает рефанд — AttackCooldownTicks = 0 (AutoAttackSystem.cs:442). Для одиночного удара это честно: свинг не нанёс ничего, значит и не потерял ничего. При серии — эксплойт: первый контакт уже записан, микростан рвёт свинг, кулдаун обнуляется, юнит выходит из стана и начинает новый свинг снова с первого контакта. Частота первого удара становится «стан + замах» вместо интервала.

Правило, снимающее это одной строкой: рефанд положен только пустому свингу.

Состояние свинга при прерыванииЧто делаем
ни одного контакта не разрешенокак сейчас: AttackCooldownTicks = 0, потери нет
хотя бы один контакт разрешёнрефанда нет; кулдаун идёт от первого контакта свинга

Тогда период «первый контакт → первый контакт следующего свинга» равен интервалу всегда, сколько бы ударов серии ни съел контроль. Микростан отнимает у жертвы хвост серии, то есть замедляет её — ровно то, чего от контроля и ждут.

  • Кулдаун остаётся замороженным на время стана (как сейчас: continue до его декремента). Альтернатива «пусть тикает под станом» делает короткие станы бесплатными по темпу и обесценивает контроль как механику.
  • Evade считается состоявшимся контактом. Связка с вердиктом (1): если промах оставить «пустым», свинг получит рефанд, и кайт начнёт ускорять атаки того, от кого убегают. Контакт засчитывается по факту разрешения, а не по факту попадания.
  • Инвариант «стан не ускоряет атаку» живёт тестом, а не комментарием: он кросс-файловый (AutoAttackSystem + AttackTiming + длительность контроля из эффектов).

Ещё вопросы, которые серия трогает молча. Первые четыре Макс закрыл в тот же день, остальные ждут захода.

  1. Два контакта в одном тике — РЕШЕНО (2026-07-30). Кадры контакта считаются в нормированном времени клипа (ClipMarkers.MarkerNormalized уже это умеет) и умножаются на attackDurationTicks — серия сжимается вместе со свингом. Дальше целочисленное округление и раздвижка: contact[i] = max(contact[i-1] + 1, computed[i]), верхний кламп — последний контакт не позже attackDurationTicks − 1 (тот же принцип, что у нынешнего клампа замаха: удар не совпадает со стартом следующей атаки).
    • Почему это обязан быть рантайм-инвариант, а не правило авторинга: свинг сжимается (attackDurationTicks = min(MaxAttackAnimTicks=30, intervalTicks)), а скорость атаки — величина рантайма, её крутят баффы. Любые два маркера при достаточном сжатии схлопнутся, и валидатор в редакторе этого не увидит. Аудит клипов остаётся второй линией: предупреждать, если маркеры стоят ближе тика уже при эталонном темпе класса.
    • Следствие — потолок скорости для многоударных китов: N контактов требуют минимум N тиков свинга, то есть атак/сек ≤ TickRate / N. При наших темпах (0.55–1.0 атак/сек, свинг 30 тиков) три контакта влезают с запасом в десять раз; правило записано, чтобы не всплыло внезапно на бафах.
    • Отбрасывать лишние контакты молча нельзя: при вердикте «on-hit на каждом контакте» потерянный контакт — это потерянный стак, то есть тихий нерф. Если контакты не влезли, это событие для отчёта аудита анимаций.
  2. On-hit на каждом контакте — РЕШЕНО (Макс, 2026-07-30): да, на каждый. Поджоги, метки, стаки льда Криоманта множатся числом ударов сознательно. Прямое следствие — пункт 1: терять контакт нельзя, и балансный заход должен считать стаки за свинг, а не за удар.
  3. Что считается «первой атакой» для заявки о скейлах первой атаки (Combat - Stats §На будущее): бонус «первая атака по цели» при серии либо срабатывает один раз на свинг, либо утраивается. Дефолт скрайба — свинг считается одной атакой; Макс заявку не помнил, вердикта пока нет, но по умолчанию берём этот.
  4. Каст между контактами — РЕШЕНО (Макс, 2026-07-30): нет, только с новой атаки. Занесённый свинг доигрывает целиком, способность не втискивается между контактами. Подтверждает существующее правило M18 и распространяет его на серию.
  5. Charge-свинг, стартующий вне досягаемости и въезжающий в неё: дистанция меняется между контактами.
  6. Hitstop и slowmo — три замирания подряд внутри одного свинга читаются как лаг; политика значимости должна знать про серию.
  7. Звук — три сэмпла подряд рискуют звучать пулемётом.
  8. Вампиризм и «дань» множатся автоматически — следствие пункта 2, отдельного решения может не потребовать, но проверить надо.

Что осталось открытым: пункты 3 и 5–8 выше — «первая атака по цели» при серии (дефолт скрайба принят по умолчанию), charge-свинг с меняющейся дистанцией между контактами, политика hitstop/slowmo, звук и множители вампиризма. Все четыре всплывут на подаче и балансе, а не в симуляции.

Владельцы правды в коде: AttackTiming.ContactTicks (раскладка контактов и оба инварианта), UnitData.HitDamageShare, тесты WindupAutoAttackTests группы «Атака из нескольких Ударов» и AnimationValidationTests.HitDamageShares_MatchMarkerCount. Инвариант «стан не ускоряет атаку» держит WindupAutoAttackTests.Stun_AfterHit_DoesNotRushNextAttack.


3.10 Четыре механики ждут общих примитивов, а три уже размножены копиями

Сверено по коду 2026-07-31 при разборе заметок Макса о ритме атаки, парировании, блоке, маскировке и рекасте. Дизайн-развилки и вопросы — Meta - Open Forks §1; здесь только то, что видно из кода: где сегодня два владельца одного правила.

ПравилоГде живёт сейчасКуда должно уехать
Рекаст авто-атакиСДЕЛАНО 2026-07-31: один глагол RuntimeUnit.RecastAttack, объявляемый флагом EmpowerNextAttackComponent._recastImmediately; ускорение фаз — две ручки SimTuningConfig. Авто-рекаст «по факту урона у активки» удалён, инвариант держит AbilitySystemTests.DamagingAbility_DoesNotSkipTheAttackQueue_RecastMustBeDeclared
Реакция на входящий удартри IPreDamageComponent с зарядами: BlockComponent (переименован из BulwarkComponent 2026-07-31), DodgeComponent, ParryComponent (2026-07-31). Заряды, ICD, фильтр прямого попадания и гейт дееспособности у каждого свои — скопированы триждывынести общий каркас «заряд + триггер + ICD», оставив компонентам только их реакцию. Пока это три копии одного пролога, а не три механики
Микро-станСДЕЛАНО 2026-07-31: общий ассет effect.micro_stun (0.4 с), на него ссылаются и «Вихревой заход», и парирование
Счёт «каждая N-я атака»EveryNthAttackComponent считает по событию урона, то есть по Ударамсерия уже в движке (§3.9), поэтому долг стал живым: Драугр на многоударном ките триггерит втрое чаще и молча. Считать Атаки
Видимость юнитаСДЕЛАНО 2026-07-31: ConcealmentSystem считает, кто скрыт; фильтр стоит в единственной точке выбора цели (ProfileBrain.SelectBest). StealthComponent по-прежнему отвечает только за то, когда Убийца уходит в тень

Все четыре механики темы сделаны 2026-07-30/31: ритм (CombatIdle), Атака из нескольких Ударов, Блок, рекаст, парирование и Маскировка.

Два долга, оставшихся от парирования:

  • Носителя нет. Примитив effect.parry не объявлен ни одним китом, то есть в бою не участвует. Заявленный носитель — Разбойник-дуэлянт, у которого пока нет карточки (Meta - Open Forks).
  • Разгон не плавный. Дизайн требует входа и выхода «не рывком», а effect.parry_haste даёт плоскую прибавку на секунду. Рампа в проекте есть ровно одна — у спринта (SimTuning.SprintRampAt), и она живёт в движении, а не в статах. Общий «баф с рампой» — отдельная задача, и до неё парирование ускоряет щелчком.

Долг Маскировки: носителей, кроме Убийцы, нет — Инвиз стоит на его «Скрытности», а ступени Слабая, Средняя и Сильная не объявляет никто. И не решено, что делают враги, когда невидимы все: сейчас они просто остаются без цели, то есть бой из двух замаскированных отрядов встаёт (Meta - Open Forks).

Остаток темы — контент, а не механика: обе новые системы ждут китов-носителей.


3.11 У скелетного рига нет клипа смерти — в слоте стоит Idle

Заглушка поставлена сознательно 2026-07-31 (решение Макса), и она видна в игре. У рига (Prefabs/Bones/) есть Idle, Walk, Attack, Attack2, Attack3, AttackCharge, Block, CombatIdle, Sprint, Stun — клипа смерти нет вовсе. Валидатор контента требует Idle/Run/Attack/Death у каждого архетипа анимаций, поэтому в слот Death у AnimationArchetypes/SwordShield.asset положен Idle.anim. (Имена обновлены 06.08.2026: UnitVisualAnimationArchetypeData, папка VisualsAnimationArchetypes, ассет BoneStandartSwordShield. Суть долга не изменилась.)

Что это значит на экране: путь смерти играет клип перед разлётом на осколки (UnitView.DriveDeath, фаза Dying), то есть скелетный юнит перед распадом будет стоять в позе покоя вместо падения. На дев-бойце это терпимо, на боевом контенте — нет.

Чинится заходом в Animation Lab: поза падения по риг-API, контактный лист на смотр, клип в слот Death. Пока клипа нет, заглушку не считать нормой — она стоит здесь именно затем, чтобы её не приняли за настройку.


3.12 LitMotion занят одним файлом — три места, где он окупится, и три, где он вреден

Сверено по коду 07.08.2026. Пакет подключён только к Guildmaster.Presentation.asmdef и вызывается ровно из Presentation/UnitView.cs (вспышка удара, сплющивание, разворот, телеграф). CLAUDE.md уже числит его «живущим точечно», а UI-анимации и боевые цифры — непереведёнными.

Куда стоит занять:

  1. Presentation/FloatingText.cs — главный кандидат. Боевые цифры крутят свой Update с ручным _elapsed, самодельным ease-out-cubic и самодельным ease-out-back с овершутом (EaseOutBack(p, _popOvershoot)). Это пуловый объект, которых в кадре бывает десятками, и каждый несёт свой Update. Твин снимает его целиком, обе кривые есть в Ease. Готча: ZoomFactor() пересчитывается каждый кадр (камера зумится), поэтому он остаётся ВНУТРИ Bind, а не берётся один раз на старте твина.
  2. Числа, меняющиеся скачком: золото после награды, полоска опыта, вместимость запаса. USS-переход тут не поможет — он анимирует стиль, а не данные.
  3. UI-джус, которого USS не умеет: овершут посадки пластины, пружина карточки мементо при взятии, тряска поля при отказе. Наведение, нажатие и появление панели USS закрывает сам и лучше — туда не лезть. Потребует дописать LitMotion в Guildmaster.UI.asmdef (сейчас ссылки нет).

Куда НЕ надо:

  • Presentation/Transition/ScreenTransitionRunner.cs — у него Tick(float dt) с явной длительностью кадра сделан ИМЕННО ради тестируемости («время — единственное, что у него снаружи»). Твин отнимет проверяемость и не даст ничего взамен.
  • Камера — ею владеет Cinemachine.
  • Симуляция — детерминизм; там нет и не будет покадровых величин.

Открытая развилка: если джус UI откладывается надолго, LitMotion можно вынести целиком, как вынесли Easy Save и Feel — один файл не окупает зависимость. Аргумент против: он zero-alloc, уже вшит в правила слоёв, а FloatingText — работа на полчаса с видимым в бою результатом.

Владелец правды: Presentation/UnitView.cs (единственный нынешний носитель), Presentation/FloatingText.cs (кандидат №1).


3.13 Сорок один журнал захода в docs/, и механически их не разобрать

Правило «леса сносятся по закрытии захода» не исполняется: в docs/ лежит 41 файл *-progress.md. Гейт ./scripts/journals.ps1 -Check при этом зелёный, и это не случайность — он мерит возраст файла по LastWriteTime, а два squash-мержа (34fecc74a от 28.07 и 67abad9c4 от 03.08) обнулили эту дату сразу тридцати файлам; у двадцати четырёх squash — вообще единственный коммит в истории dev.

Разобрать пачкой нельзя, и это проверено на себе 07.08.2026. Признак «в файле нет незакрытых чекбоксов» кажется годным и негоден: в восьми отобранных по нему файлах не было галочек вовсе, а по содержанию среди них оказались skeletal-animation-progress.md (853 строки, статус «в работе», открыты шаги 5 и 7), ap-and-skills-progress.md (404 строки, статус proposed — числа ждут вердикта Макса), performance-progress.md (начат за день до того) и meta-progression-progress.md, на который ссылается живой журнал ГД. Снос по признаку потерял бы и незакрытые заходы, и вопросы, адресованные человеку.

Снят по итогам ровно один — ui-layout-handoff-progress.md: он сам объявлял себя лесами, перечислял закоммиченное и указывал два журнала, в которые уехала правда (оба на месте), и ни один файл на него не ссылался.

Что делать:

  1. Разбор — заход с чтением каждого файла, а не скрипт. По каждому: заход закрыт? невыполненное переносится в tech-debt или в открытые развилки ГДД? кто-нибудь на него ссылается? Коопные журналы (coop-*, qa-coop) в этот заход НЕ берутся, пока идёт работа по сети.
  2. Гейт починить — мерить возраст по дате последнего содержательного коммита (git log -1 --format=%ad -- <файл>), а не по mtime, и дополнительно ловить самообъявленное закрытие внутри файла. Порог по mtime вдобавок вот-вот сработает вхолостую: шесть журналов уже на 10–13 днях.

Владелец правды: scripts/journals.ps1, сам каталог docs/*-progress.md.


3.16 «Читает мир и пишет в него в одном обходе» — класс, давший четыре расхождения зеркала

Сверено 09.08.2026 после четвёртого случая. Система идёт циклом по юнитам, внутри цикла пишет состояние, а следующая итерация читает уже изменённый мир. Порядок обхода у отражённых сторон ОДИНАКОВЫЙ, а не зеркальный, — поэтому сторона, идущая первой, получает преимущество, и зеркало разъезжается.

Три первых случая породили сам сторож MirrorMatchTests (см. его <remarks>), четвёртый — проход блинков в AutoAttackSystem (журнал «One Path Knew About The Rake»).

Вылечены и служат образцом: MovementSystem (два прохода через буфер _next), SeparationSystem (вклады копятся, применяются после обхода), CombatSimulation.ApplyPendingTeleports (все точки от снимка, потом запись), AutoAttackSystem (то же, с 09.08.2026).

Остался один кандидат — DisplacementSystem. В цикле по _active пишется u.Position, и там же Cannonball читает мир: второй летящий в том же тике увидит уже сдвинутого первого. Замером НЕ подтверждено: зеркальные тесты зелёные, включая дуэль Монаха вихря против себя, где оба тела летят одновременно. Это подозрение по форме кода. Чинить — только после того, как найдётся состав, который расходится: правка ради формы здесь стоит дороже, чем выглядит (ядро бьёт по линии полёта, и порядок применения меняет, кого задело).

Как искать следующий случай — метод, который сработал: сузить составом (все пары и тройки из расходящейся четвёрки), потом читать полный отчёт MirrorFixture — он печатает слепок обеих сторон и журнал ударов тика. Гипотезы по коду до этого отвергались замером дважды подряд.

Владелец правды: MirrorMatchTests (сторож и история класса), Combat/Systems/MovementSystem.cs (образец двух проходов).


3.15 Vector2.right фолбэком — одиннадцать мест, и каждое это мина под зеркалом

Найдено 08.08.2026 при разборе расхождения зеркала. В боевом коде одиннадцать вырожденных веток вида dir = v.sqrMagnitude > 1e-6f ? v.normalized : Vector2.right. Правильно сделана ровно одна — FleeSteering.HomeDir (unit.Team == 0 ? Vector2.left : Vector2.right): она выбирает направление по стороне команды, поэтому у отражённых сторон получается зеркально.

Остальные берут мировое направление. Пока ветка недостижима, всё тихо; как только она сработает у обеих сторон — обе поедут ВПРАВО вместо противоположных, и зеркало разойдётся. Ровно этот механизм уже ловили в расталкивании (остаток BAL-014): там его вылечили DegenerateDir, и приём с тех пор в проекте есть.

Семь двигают тело или решают исход — это и есть мины: DisplacementSystem.cs:66 · AbilitySystem.cs:496 (бросок), :502 (рывок), :761 · ChargeThroughOnBattleStartComponent.cs:87 · ThrowAllyComponent.cs:85 · DodgeComponent.cs:124 (кувырок) · плюс CombatSimulation.cs:532 (скорость снаряда без цели), :609 (направление линейного запроса) и CombatPositioning.cs:157 («в спину» — от него зависит бонус за удар в тыл).

Три безобидны: AreaHit.cs:34 и WhirlDashLandingComponent.cs:76 — дев-оверлей и показ.

Чинить не пачкой. У каждого свой вопрос «а куда по умолчанию»: кувырку — вперёд или к тылу, броску — от кого, запросу линии — есть ли вообще осмысленный дефолт или это признак того, что звать его в таком состоянии нельзя. Пачка одинаковым HomeDir закроет тест и спрячет три разных решения под одно.

Готча на будущее: достижимость ветки неочевидна. У кувырка стоит фильтр «только автоатаки» (DodgeComponent.cs:72), из-за которого источник удара есть всегда, — то есть его ветка выглядит мёртвой, хотя intent и позиция атакующего могут совпасть. Прежде чем объявлять такую ветку недостижимой, нужен замер, а не рассуждение.

Владелец правды: Combat/FleeSteering.cs (HomeDir — образец), Combat/Systems/SeparationSystem.cs (DegenerateDir — как это лечится), тест MirrorMatchTests.


3.14 Что панель аудита 07.08.2026 не посмотрела — и почему это важнее половины найденного

Восемь линз прошли по коду, данным, тестам и докам. Критик полноты назвал дыры; полный разбор — docs/audits/2026-08-07/coverage-gaps.md. Здесь то, что стоит отдельного захода.

1. Строковый контракт UXML × Q<>("…") — второй по величине после локализации, и без стража. 148 строковых поисков элементов в Scripts/UI и Scripts/Presentation против 189 уникальных name= в разметке. Компилятор к этому шву слеп: переименованный элемент даёт молчаливый null и мёртвую панель в рантайме. Локализацию мы закрыли гейтом (UiLocalizationCoverageTests) — здесь нужен такой же, и делается он тем же приёмом.

2. Префабы и сцены не читал никто. Двадцать префабов, пять сцен. Минимум два вердикта панели висят на этом условно: «не проверял, назначен ли _unitViewPrefab в сценах билда» и «_spatialHashCellSize проверен только со стороны кода». Незаполненный [SerializeField] — ровно тот класс дефектов, который панель искала в коде и физически не могла увидеть в YAML.

3. Полоса, где теряются данные игрока. Все DTO сейчас [SaveSchema(1)], миграций нет: один бамп версии — и каждый существующий сейв становится Unsupported. Сам сервис честен и громок, но куда результат уходит дальше и что игра делает с Unsupported / TooNew / Corrupted, не проследил никто. Экрана «сейв заблокирован» тоже нет — вместо него лог (GameFlow.cs:263-268).

Не смотрел никто: весь Art/** (2553 файла: шейдеры, материалы, импорт-настройки), 106 файлов редакторного кода — того самого, который пишет ассеты, внутренности Presentation/** (20 Update на частоте рендера в мире, который по конституции не выгружается), кластеры Tags/AiPresets/Keywords/Species/Outfits/Vfx, Tests/PlayMode/Battle.

Классы проблем, которых не искала ни одна линза: производительность и аллокации, утечки подписок (в непрерывном мире это первый класс дефектов), импорт-настройки ассетов, мёртвые ассеты по guid, состав билда, доступность (colorblind, fontScale, rebind — ноль вхождений во всём Scripts), корректность редакторного тулинга.


§3.21 и §3.22 закрываются заходом по иерархии скоупов, принятым Максом 02.08.2026 — Planning - Scope Hierarchy §6. Отдельно их не чиним: корень у обоих один — у боевого скоупа нет границы боя. Очередь команд сносится в фазе 1 («никакого легаси»), сброс у гостя исчезает сам, когда лента начнёт рождаться вместе с боем.

3.21 Очередь команд симуляции осталась без единого пользователя

Открылось 02.08.2026 при переписывании сетевой паузы. CombatSimulation.EnqueueCommand, ApplyDueCommands, ICombatCommand и обе его реализации (PauseCommand, ResumeCommand) существуют с Фазы 1, но звал их только NetworkCommandRelay — удалённое lockstep-легаси. В продакшн-коде пользователей теперь ноль; держат механизм два теста (CombatSimulationTests.PauseCommand_StopsTick и соседний), то есть он проверяется ради себя самого.

Заодно от него зависит ветка в CombatSimulation.Tick: «пока в очереди есть команды, счётчик тиков идёт даже на паузе, иначе ResumeCommand с будущим TargetTick никогда не наступит». Без очереди эта оговорка не нужна.

Почему не снесла сразу: это правка боевого ядра, а не сети, и у неё есть дизайн-сторона — отложенная команда на будущий тик была бы швом под приказы игрока в бою (Manual Action Charges, ГД: Roadmap Блок 3, отложены Максом). Решение «снести и завести заново, когда понадобится» против «оставить пустой механизм» стоит вердикта, а не срезки в чужом заходе.

Что известно точно: пауза игрока — свойство показа (TimeScaleService), сетевая пауза — тоже (BattleControlRelay), так что паузу через очередь команд не вернёт никто. Если механизм оставляем, обе команды всё равно лишние.

3.22 Повторный бой у гостя: сбрасывать приёмник и реестр некому

Открылось 02.08.2026 при проводе коопа в бой. У гостя нет тикающей CombatSimulation, значит OnBattleReset у него не стреляет — а на этом событии висят ровно те сбросы, которые нужны новому бою: TapeIntake.Reset (забыть применённые номера чанков) и BattleUnitRegistry.Clear (забыть состав). Хост при рестарте начинает нумерацию с нуля, гость примет чанк №0 за дубль и нового боя не увидит вовсе — тихо, без единой ошибки в консоли.

На первый бой сессии это не влияет, поэтому и не блокирует: вертикаль проверяется одним боем.

Способ починки изменился 02.08.2026. Прежний план — слать сигнал «бой начался заново» и сбрасывать обе половины на приёме — больше не нужен: у гостя бой теперь тоже скоуп, а новый бой это новый скоуп. Приёмник и реестр рождаются вместе с ним, и забывать им нечего по устройству. Значит долг закрывается не сообщением про сброс, а сообщением про границы боя — «бой открылся / закрылся», — которое гостю нужно и само по себе: без него он не знает, когда открывать арену (шаг «мероприятие у гостя»).

Владелец правды: Net/Tape/TapeIntake.cs, Combat/Tape/BattleUnitRegistry.cs, Net/BattleControlRelay.cs; состояние захода — docs/coop-slice-progress.md.

3.23 IBattleAuthority спрашивают каждый кадр, хотя роль уже известна на входе — ЗАКРЫТ 02.08.2026

Закрыт в тот же день, что и открылся. Состав боевого скоупа выбирается ролью сеанса (CombatLifetimeScope.RegisterCoop): владелец получает петлю тика, стример, вещателя и объявителя состава, гость — приёмник чанков, его насос, приёмник состава и GuestPlaybackLoop вместо цикла. IBattleAuthority, NetBattleAuthority и BattleRole удалены; BattleRosterRelay, делавший обе стороны в одном классе, разделён на BattleRosterAnnouncer и BattleRosterIntake. Соло отдельной ролью НЕ стало: это тот же владелец, просто без поднятого транспорта — разница между «играю один» и «играю хостом» вся в том, есть ли кому слать. Иначе включение лобби посреди забега требовало бы переоткрыть сеанс. Единственная оставшаяся проверка живёт у вещателя и спрашивает соединение, а не роль.

Открылось 02.08.2026 вместе со скоупом Сессии. NetBattleAuthority выводит роль узла (соло / хост / гость) из транспорта и отвечает на вопрос при каждом обращении; вокруг него живут ветвления в CombatLoopService, BattleTapeBroadcast, BattleRosterRelay, TapeIntakePump.

Обоснование у этого было честное и записано в самом корне: боевой скоуп поднимался на буте, когда сети ещё нет, — решить роль регистрацией было нельзя. Причина отпала. Бой рождается по требованию внутри мероприятия, мероприятие — внутри сессии, а у сессии роль выбрана в момент открытия (SessionRole) и за её жизнь не меняется: сменить роль можно только новым сеансом.

Значит боевой инсталлер может собирать РАЗНЫЙ состав по роли — вещатель ленты у хоста, приёмник у гостя, ни того ни другого в соло, — и ветвления исчезают вместе с самим интерфейсом. Заход трогает весь Net-слой и потому отложен: он не мешает ни соло, ни текущей кооп-вертикали.

Владелец правды: Core/Net/IBattleAuthority.cs, Net/NetBattleAuthority.cs, Game/Session/SessionInstaller.cs; план уровней — 40-planning/scope-hierarchy.

3.8 Реестр внешнего аудита (2026-07-10)

Сведено из пяти аудитов (docs/audits/), дедуплицировано, каждый пункт сверен с рабочим деревом. Статусы: ✅ подтверждён по коду · 🔍 правдоподобно, нужна проверка · ⚠️ устарел/неверен на текущем дереве · 🔗 уже описан выше.

Подтверждённые баги (это НЕ отложенный долг — реальная корректность): — B1–B7 исправлены 2026-07-10, см. §2.5.

IDБагГдеСтатусПриоритетНашёл
B1Рестак щита пере-вычитает: Reapply зовёт OnExpire с новым Stacks, Mathf.Max(0,…) маскирует, но съедает вклад второго источника щитаEffectSystem.cs:375-406, ShieldComponent.cs:27✅ исправлен §2.5P0Claude Opus (только она)
B2Рестак Dodge бесплатно перезаряжает все заряды негейта: OnApply безусловно new int[maxCharges], Reapply его зовёт. Нюанс: только Stack/StackAndRefresh, чистый Refresh не триггеритDodgeComponent.cs:27, EffectSystem.cs:389-406✅ исправлен §2.5P0Claude Opus
B3Корень B1/B2: Reapply = слепой OnExpire→OnApply ради смены числа стаков; per-component state живёт в общем RuntimeEffect. Фикс — OnStacksChanged(old,new) + вынести stateEffectSystem.cs:389-406, RuntimeEffect✅ исправлен §2.5 (дельта-шов; вынос state отложен)P0Claude Opus
B4Kite-поля мёртвые: MoveKite использует магическое range*0.6f + AttackRange, а Kite.FleeDist/FallbackDist из AIProfile нигде не читаются. Данные выглядят рабочими — но no-opMovementSystem.cs:85-100, AIProfile✅ исправлен §2.5P1GPT-5.6
B5Broad-phase линейного запроса теряет цели в дальних углах: радиус length вместо length+halfWidth → юнит на (≈length, ≈halfWidth) отсекается до narrow-phaseCombatSimulation.QueryUnitsInLine (~:295)✅ исправлен §2.5P1Claude Opus
B6DrainEventQueue после капа MaxEventsPerDrain=512 делает silent Clear() — исход боя зависит от «уложились ли в 512». На текущем контенте 512/тик недостижимо → future-major, не critical-сейчас. Фикс: dev-assert + логCombatSimulation.cs:441✅ исправлен §2.5P1Grok, GPT
B7Iron Spearman: _resourceType: None, но _resourceOnHit:5, MaxResource:30, активка стоит 30 — противоречивый контракт ресурсаIronSpearman.asset✅ исправлен §2.5 (Rage)P1GPT-5.6

Консистентность данных (ретрофит дёшев, пока ассетов мало):

IDНаходкаСтатусПриоритет
C1Seed боя через UnityEngine.Random+UtcNow (CombatLifetimeScope.cs:113-117); нарушает «рандом только через IRngService»; блокирует репро/реплей. gm_rng_seed — stub (только лог)ЗАКРЫТО (сверено 07.08.2026)
C2Нет OnValidate/content-validator на SO — битый контент проходит молча (Kite Fallback>Flee, resource/cost, диапазоны 0..1, uniq id)P1 (расширяет )
C3armorK в двух местах: сериализован на CombatLifetimeScope и в StatsConfigЗАКРЫТО 30.07 — боевой скоуп больше не владеет StatsConfig, см. §5.4
C4buff/debuff смоделирован трижды (EffectPolarity + EffectTag.Buff/Debuff + DispelTargetPolarity) — два источника правды на ассетеP2
C5Два enum таргетинга (AbilityTargetMode vs TargetingMode) + устаревший докблокP2
C6Две модели скейла: ScalableValue (эффекты) vs сырые _damageMultiplier/_healFlat (активки) — payload активок не переиспользует шовP2

Гигиена / мёртвый код / швы:

IDНаходкаСтатусПриоритет
H1Мёртвый DamageNumber+DamageNumberSpawner (внутри — set через рефлексию, докстринг врёт про LitMotion); живой путь — FloatingText+пулЗАКРЫТО (сверено 07.08.2026: классов в дереве нет)
H2ISimEvent — маркер без реализаций (CombatEventData его не реализует)P2
H3Двойной канал наружу: C#-events + MessagePipe; контракты событий объявлены в тяжёлом Guildmaster.Presentation → вынести в нейтральный слойP2
H4alpha интерполяции frame-basedЗАКРЫТО (сверено 07.08.2026)
H5Stats.RebuildCache — массивы на пересборку🔗 (жив; их пять, а не три)P2
H6GlobalMessagePipe.SetProvider — мёртвая глобальная поверхность (никто не читает)ЗАКРЫТО (сверено 07.08.2026)
H7RuntimeUnit — открытый мешок mutable-полей во view (инвариант «view только читает» держится на дисциплине)P3
H8MarkTransferComponent аллоцирует new List<RuntimeUnit> на каждую смерть носителяP2 (по касанию Следопыта)
H9Cooldown активок во float-секундах (AbilitySystem), а не int-тиках — дрейф из принятой tick-дисциплиныP3
H10~1 ГБ мусорных worktrees в .claude/worktrees/ (detached, от прошлых сессий)⚠️ неверен на текущем дереве (сверено 07.08.2026)

Инфраструктура / инструменты:

IDНаходкаСтатусПриоритет
I1run-tests.ps1 хардкодит Unity 6000.0.23f1, проект на 6000.4.8f1 → локальный прогон падает Unity not found. Читать версию из ProjectVersion.txt✅ исправлен §2.5P0 (5 мин)
I2Билд первой сценой ставит CoreScene (EditorBuildSettings), GPT утверждает что она пустая, а root — в BattleScene → boot-flow не замкнут. Запуск из BattleScene — осознанный dev-harness✅ снят 2026-07-26: CoreScene держит [Root] и [Bootstrap], обе игровые сцены грузятся с бута — flow замкнут (сцены проекта (Assets/_Project/Scenes/))P3

Устаревшее в аудитах / неверное на текущем дереве:

IDКлеймРеальность
X1«EditMode красный, падает SerializeReferenceSpikeTests из-за пути Assets/Tests/EditMode» (GPT, Codex)GPT/Codex правы на fd858f8 — тест молча падал (путь стал stale после переноса в 143535d). Починен в незакоммиченной арена-работе (:20 теперь Assets/_Project/Tests/…, suite 184/184). Т.е. старый клейм «171/171 green» был неверен; текущее рабочее дерево зелёное
X2«Арены нет в коде, ±200 хардкод, spawn из кода» (Composer)⚠️ Арена закодирована в рабочем дереве (uncommitted): ArenaLayoutData/ArenaBounds/DeploymentService
X3§3.4 этого дока ссылается на TargetingSystemУчтено 07.08.2026: §3.4 переписан, класса нет с Фазы 3, перебор живёт в ProfileBrain. Строка оставлена как след: реестр полтора месяца спорил сам с собой в двух своих разделах

Сетевой долг (host-auth vs lockstep-наследие NetworkCommandRelay, authority-гейт тика, полный порядок команд (tick,player,seq), float-checksum) — все 5 подтвердили; относится к 4. Главная будущая таска (вынесено отдельно), не трогаем в одиночке.

3.20 Бут-экран: геймпадом не закрывается, нажатие молчит, три ключа не заведены

Найдено 31.07.2026 при разборе старта игры; сверено по живому коду, взято в работу не было.

  • С геймпада титул не закрыть — это блокер, а не мелочь. TitleCardScreenView слушает PointerDownEvent, ClickEvent и KeyDownEvent; геймпад в UITK приходит навигационными событиями, а NavigationSubmitEvent не обрабатывается нигде в проекте (ноль вхождений при живом InputSystemUIInputModule в CoreScene). Кнопки меню выживают только потому, что UITK-кнопка ловит Submit сама. Игрок с геймпадом застревает на первом экране игры.
  • Первое действие игрока не отвечает звуком. Появление титула озвучено (menu.title_card.stinger в RunAudioPresenter), а dismiss — нет, хотя остальные экраны звучат на действие (camp.action.ui, chest.open.stinger).
  • ui.boot.title / ui.boot.hint / ui.boot.loading в таблице UI не заведены — экран держится на RU-литералах в коде. ui.boot.legal заведён 31.07, соседние трогать не стали.
  • Мельче: спиннер декоративен (реального прогресса загрузки нет — шов понадобится, когда вырастет контент), версия показана только в главном меню, курсор системный (defaultCursor пуст), профиль создаётся молча как «Профиль 1» — экрана первого запуска нет.

Открытая возможность оттуда же: своя двухфазная заставка вместо Unity-сплэша (знак студии → титул, обе фазы скипаются) — см. Journal - Splash Shows The Studio Once.


4. Главная будущая таска (вынесено отдельно)

🎯 Сетевой кооп — host-authoritative (Фаза MP). Репликация состояния юнитов через NGO, клиент рендерит из реплик-состояния, гейт тика на хосте, релей команд для полного ввода, синхрон паузы, late-join/реконнект, сид боя от хоста. Полный состав — Journal - Host-Authoritative, Not Lockstep §4. Сейчас не трогаем (в одиночке сеть спит).

Уточнено 01.08.2026: авторитет хоста остался, а «репликация состояния юнитов через NGO каждый тик» из состава выбыла — бой едет чанками ленты (§4.1). Что делаем вместо этого и в каком порядке — Planning - Coop Vertical.

4.1 ЗАКРЫТО 01.08.2026: боевая лента раздаётся чанками — события плюс сжатые снимки

Вердикт и его ценаJournal - The Tape Ships As Events Plus Packed Snapshots; формат чанка, лимит и порядок работ — Planning - Coop Vertical. Ниже сохранён разбор развилки: он объясняет, почему третий вариант нельзя было отвергнуть причиной lockstep-а, и чем в итоге пришлось заплатить.

Поднят дизайном (кооп-кластер ГДД), вердикт был за тех-стороной. Формулировка-источник — Coop - Sync Model, раздел «Класс 1».

Механизм наполовину есть: локально сим пишет боевую ленту, показ читает её с лагом — события лежат за весь бой, снимки состояния идут кольцевым окном вокруг момента показа. Сим уже умеет уезжать вперёд хоть до конца боя. Вопрос в том, что именно летит по сети:

ВариантПлюсЧем упирается
состояние каждый тик (как предполагает решение 19.06)ничего не менять в моделисамый дорогой по трафику
события ленты одним пакетомдёшевы и уже хранятся целиком: «кино» раздаётся за секунду до боянужен контракт сериализации события и порядок доставки
вход + сид, бой проигрывается локальноминимальный трафиктот же float, из-за которого отклонён lockstep

Отличие от lockstep, из-за которого третий вариант нельзя отвергать той же причиной: здесь расхождение — не «кошмар отладки», а разные картинки без последствий для правил, потому что исход считает хост. Цена ошибки другая.

Что обязан соблюсти любой вариант (дизайн-требования того же раздела): все видят один и тот же бой — тайминги, смерти, исход; пауза общая и видно, кто нажал, а скорости показа не существует вовсе; камера личная; отстающий по показу догоняет сам, а не тормозит группу; задержка перед боем допустима, внутри — нет; пока открыты ставки, лента не уходит игрокам даже фрагментом.


5. Хвосты закрытых заходов

Пункты пережили заходы, которые их родили; сами журналы заходов удалены 30.07.2026 как леса.

5.1 ЗАКРЫТО 30.07: реализационные скиллы сверены с кодом (5 из 5)

Скиллы предписывают, а не описывают, поэтому заморозка справочника их не касается: расхождение с кодом здесь настоящий дефект — агент получает инструкцию, которой код не соответствует.

СкиллСтатус
uitkсверен 30.07 — витрина-фантом UiGalleryScreen.uxml заменена на стенд UiPreviewCatalog, сняты IMenuRouter и LoadoutHubView
audioсверен 30.07 — снята заглушка UnityAudioService (удалена 26.07), единственная реализация FmodAudioService
data-authoringсверен 30.07 — папки Data/Editor/Migrations/ нет, массовая правка идёт через ContentEditService/ContentEditBatch/ContentCrudService
gamefeel-vfxсверен 30.07 — шов префаб-VFX числился «не построенным», PixelBurst* и NullScreenShake удалены из описания как удалённые из кода
combat-simсверен 30.07 — один мёртвый путь (PixelBurstPreset); API-утверждения сошлись с кодом

Пункт закрыт: 5 из 5. Возвращаться к нему при следующем крупном рефакторе подсистемы — метод сверки описан выше и занимает минуты, а не заход.

Метод, который сработал: вытащить из скилла все имена в бэктиках и все пути, сверить с индексом типов кода и с файловой системой. Ловит именно дрейф и не требует читать подсистему целиком. Готча: не считать дрейфом Unity/FMOD API, методы (индекс типов их не содержит) и то, что скилл сам помечает как удалённое или как ещё не реализованное.

5.4 ЗАКРЫТО 30.07: дубль конфигов снят переносом ссылок в ассет

StatsConfig и ClassBalanceConfig объявлены полями в двух скоупах: Root собирает из них превью статов для интерфейса, боевой скоуп берёт константу брони и реген ресурса и отдаёт их фабрике. Сам SO в контейнере не лежит нигде — дублируется только сериализованная ссылка сцены, и расхождение держит SceneWiringTests (требует один и тот же ассет во всех сценах).

Свести к одной регистрации мешает то, что боевой скоуп обязан подниматься без Root: standalone dev-арена запускается отдельной сценой, и без этих полей симуляция не соберётся вовсе. Решено 30.07 третьим путём: ссылки на StatsConfig/ClassBalanceConfig уехали из сцен в GameConfig, скоупы и бенчи берут их оттуда. Дубль ссылки на один ассет остался, но второго владельца факта больше нет. Тем же движением закрылся C3 (armorK в двух местах) и перестали угадывать бенчи. Провенанс и цена — Journal - Config References Live In The Asset. EditMode 775/775.

5.5 Инвариант «Combat не знает MessagePipe» держится соглашением, а не тестом

Боевая сборка отдаёт наружу голые C#-события и не зависит от пакета шины — проверяется тем, что using MessagePipe не встречается в Scripts/Combat ни разу. Первый, кто его там напишет, ничего не сломает и не заметит. Нужен guard-тест на границу сборок (по нашему правилу кросс-файловый инвариант живёт в тесте). Почему так устроено — Journal - The Bus Stops At The Combat Assembly.

5.6 В докстрингах кода живут ссылки на мёртвую нумерацию вики — и их становится больше

«вики "12" §2.4», «вики "13" §3.1» и подобное. Такой раскладки нет с 16.07.2026 (вики перевезена в Diátaxis-кластеры со слагами), а после 30.07 половина целей ещё и заморожена. По правилу «код владеет правдой о коде» такие ссылки не нужны в принципе: это остаток модели, где объяснение жило в вики. Чинить массово — вырезать упоминание, оставив сам текст докстринга; смысл не теряется, потому что текст самодостаточен.

Число здесь не пишем — оно само становится вторым владельцем факта. В пункте полтора месяца стояло «221 строка в 133 файлах»; замер 07.08.2026 дал 238 в 151, то есть долг не чинили и он рос, а записанное число выглядело стабильным. Мерить так:

rg 'вики «\d+»' Assets/_Project --stats

Долг течёт по своей же причине: код пишется «как соседний» (code-standards §2), поэтому новая правка легально копирует ссылку из строки рядом. Массовая чистка без guard-теста даст ту же картину через месяц.

5.2 Дыра журнала решений: 12–16.07 и 20–25.07

За эти два окна нет ни одного «почему», а развилок было много: persist-мир, UI-реворк, модель урона, каскад статов, теги юнита, VFX-шов, карта акта, палитра. Восстановимо, пока помнится — потом только из кода, где «почему» не лежит. Заход: git log по диапазону → запись-файл на развилку в 00-meta/journal/. Порог — как в CLAUDE.md.

5.3 Реестр внешних аудитов остаётся на диске

docs/audits/ (216 КБ, три даты) не удалён осознанно: он — evidence под открытые пункты §3.8 (C1–C6, H1–H10). Удалять после того, как эти пункты закроются или будут признаны неверными.

5.7 «Спереди» имеет двух владельцев: приватный InFront и конвенция показа

Facing/InFront живут приватно в ParryComponent (сектор из _frontalDegrees, направление — по цели, а если её нет, по движению). Показ выводит «спереди» по своему правилу, и совпадение держится только комментарием в том же компоненте: «здесь берётся то же правило, чтобы “спереди” в бою совпадало со “спереди” на экране». С решением 2026-07-31/92 (направленный блок) правило понадобится третьему потребителю — BlockComponent, — и копия появится сама.

Чинить так: вынести направление и фронтальный сектор в общий помощник боевой сборки, оба компонента читают его; сверху guard-тест «спереди в симуляции = спереди на экране», потому что это кросс-файловый инвариант, а такие у нас живут в тесте, а не в комментарии. Ориентации как состояния в RuntimeUnit по-прежнему нет и заводить её не нужно — правило вывода одно, владелец должен быть один.

5.8 Хвосты захода по UI-состояниям (06.08.2026)

Заход закрыл перечень элементов, гейты и расчистку отменённого; ниже — то, что он вскрыл, но не починил. Каждый пункт назван вслух, потому что молчаливый долг здесь уже стоил полутора месяцев жизни отменённого регистра.

1. UiPreviewCatalog пережил свою задачу и теперь ВРЁТ — ЗАКРЫТО 06.08.2026. Галерея компонентов (["gallery"], 153 строки вместе с витриной подсказок) вырезана: она показывала 7 позиций из 14 контролов и собирала вкладку голым Button там, где экраны используют gm:PlateButton. Её задачу делает контактный лист (Alebardium → UI → Contact Sheet), и делает верно, потому что строит образцы через UiSampleFactory.

Сам UiPreviewCatalog ОСТАЛСЯ и нужен: он собирает одиннадцать ЦЕЛЫХ ЭКРАНОВ на стендовых данных (лавка, награда, событие, сундук, итог, меню, Двор, дев-консоль…) — «посмотреть экран, не гоняя весь флоу». Его зовут DevEncounterPanel, UiPreviewHost и UiPreviewMenu. Формулировка «снести витрину» была неточной: врала одна её часть, а не инструмент.

2. Восемь экранов лепят разметку в C#ProfileScreenView, GuildSelectScreenView, ShopScreenView, RewardScreenView, EventScreenView, CampScreenView, LoadoutInventoryView, PeerLostDialogView. Это прямое нарушение правила «разметка и стиль — только UXML/USS». Цена уже уплачена: витрина лавки строит свою карточку вместо RelicCard, поэтому её состояния пришлось писать отдельными правилами в screens/shop.uss вместо наследования от карточки.

3. «Таб» по-прежнему существует в трёх классахgm-tab (пластина), gm-filter-tab и gm-runbar__tab (оба чипы). Гейт состояний их закрывает через поле Base, то есть они больше не расходятся молча, но единого источника у них нет. Заявка Макса от 24.07.2026 дословно: «все 3 (теги, табы инвентаря и верхней панели) — должны иметь один источник правды. Просто разные режимы работы». Не сделано.

4. Роль --gm-color-focus-ring осиротела. Фокус везде рисуется --gm-color-hover-border (тот же знак, что у наведения) — это осознанно и консистентно, но роль под фокус заведена отдельно и не используется. Либо занять её, либо снести: сейчас она читается как «фокус имеет свой цвет», чего в дереве нет.

5.9 Типографика привязана к экранам — ЗАКРЫТО 06.08.2026

Сведено в тот же день. Девять ролей (gm-text-display / -title / -subtitle / -name / -body / -caption / -note / -label / -code) держат гарнитуру и кегль, семь меток (gm-text--*) — цвет, класс экрана — только раскладку. Держит UiTypographyGateTests.

Что осталось от пункта: карточки и строки живут той же болезнью — gm-shop__card рядом с gm-card, gm-shop__stash-row рядом с gm-reward-drop__row (см. §5.8 п.2). Приём отработан трижды, осталось применить.

Урок, который стоит помнить при следующем сведении: ярус гарнитуры — ОТДЕЛЬНЫЙ владелец, и по инвентарю «кегль + цвет» его не видно. Групповой селектор в components/text.uss назначал сериф списку блоков, из-за чего gm-shop__section-title по числам выглядел «телом текста», хотя это антиква. Перевод на роль-гротеск сменил бы гарнитуру молча. Отсюда девятая роль (-subtitle) и обязательная чистка яруса: класс, получивший роль, из списка убирается.

5.10 Хвосты захода по контактному листу (07.08.2026)

Заход закрыт, леса docs/ui-sheet-rework-progress.md снесены. Осталось три вещи, каждая ждёт глаза, а не работы:

1. Экраны после смены рампы поверхностей не смотрены в игре. Фон панели ушёл на 18%, кнопки и чипы встали на raised 26%, утопленное — на 15%. Числа контраста считаны и записаны в журнал «The Button And Its Ground Were The Same Colour», но вместимость и читаемость — разные вопросы. Глубину можно двигать по кадру Alebardium → UI → Lightness Ladder.

2. Карточка Мементо 112x168 не смотрена в игре. Размер выведен из содержимого (имя не влезало ни одно, спрайту оставалось около 20px), высота грида в LoadoutScreen.uxml пересчитана под два ряда. Если панель распирает — двигать оба числа вместе, они связаны комментарием с обеих сторон.

3. Тёплая половина двуцветия — это арт, а не тема. Договорённость 06.08: интерфейс синеватый, мир тёплый. Тема свою часть держитне держала до 21.08.2026: поверхности оставались тёплыми (тон 32°), холодным был только акцент, и запись эта врала три недели. Тема свою часть выполнила 21.08 — рампа ink переехала на 188° (запись). Ждёт художника по-прежнему тёплая половина: фоны мира, текстуры и обрамление.

5.11 Хвосты захода по дуге за клинком (07.08.2026)

Заход закрыт, леса docs/bloom-showcase-progress.md снесены. Что сделано и почему — четыре записи журнала за 06–07.08: шов стенда, след во времени, раскадровка и фиктивное время, направление клинка. Осталось пять вещей.

1. Громкость блума — ЗАКРЫТО Максом 07.08.2026. Вердикт: «Блум я настроил так как считал нужным, все хорошо». Лечило растекание — scatter: 0.12 в Assets/Settings/PostFX/BattlePostFX_Base.asset, — а не громкость: жалоба была «ореол большой, мылит», и порог с интенсивностью тут ни при чём. Заход начинался именно с этого вопроса и три дня шёл мимо него: чинились дуга, стенд и риг, потому что эффекты, по которым судят о свете, показывали неправду.

2. Клинок нарисован по диагонали своего кадра (40.1°). Направление меча в мире = поворот узла арта (29.38°) плюс эта диагональ. Код от этого больше не зависит — направление берётся с рисунка, каким бы он ни был, — но «наклон меча одним читаемым числом» до перерисовки недостижим. Работа художника; после неё поворот узла станет единственным владельцем наклона.

3. Предплечье и клинок под 132° друг к другу (локоть→кисть смотрит на −67°, клинок на +65°, поза покоя). Макс правил рисунок кисти Hand_R_Art (25.36°) — держится меч теперь красиво, но соосности «предплечье → меч» это не дало. Нужен ли такой инвариант вообще — вопрос без вердикта: дуге он не нужен, она берёт плечо и остриё.

4. Позы четырёх клипов после переноса поворота никто не смотрел глазами. Idle, Sprint, Block, Stun — кривые сложены математически точно (значения и тангенсы), тесты зелёные, но кисть теперь несёт поворот меча, и это ВИДНО. Атака проверена раскадровкой, остальные нет: стенд показывает эффекты, а не анимации.

5. Лестница яркости эффектов не сделана. Семь множителей свечения лежат в коридоре 0.8–2.2 — меньше полутора стопов, — и вспышка смерти при этом самая тусклая из всех. Правило ступеней записано (code-standards §9), развести по стопам — отдельная работа.