Трендовые github проекты в нашем телеграм канале. Подпишись → Восемь сит для выдуманного кода
Выдуманная функция FrameAllocator::TryReclaimTail() с правильным префиксом, в стиле проекта и с поясняющим комментарием проходит ревью без вопросов. Если вызов при этом сидит внутри профайлерного макроса, который на десктопе разворачивается в пустоту, сборка под PC тоже зеленеет. Ошибка всплывает через два дня на ночном билде под консоль: линковщик не находит функцию, которой в движке никогда не было.
Это типичная картина для старого проекта, куда код пишут вместе с LLM. Стиль кодовой базы модель схватывает за пару файлов, а что в ней реально объявлено, помнит плохо. Явные ошибки ловит компилятор, правдоподобная выдумка проходит мимо него и мимо уставшего ревьювера. Запрещать модели бесполезно: ими продолжат пользоваться, просто перестанут признаваться. Ниже цепочка проверок для C++-движка с MSVC, clang и gcc, которая за полгода научилась ловить большую часть таких выдумок.
Сборка всеми тулчейнами сразу
На обычную сборку приходится больше половины пойманных выдумок, и настраивать её дешевле всего. Каждый PR собирается всеми тулчейнами проекта (MSVC для десктопа и редактора, clang под две консоли, gcc для сервера) с флагом «предупреждения как ошибки». Консольные билды раньше ездили ночью, теперь PR ждёт все платформы, и ферма стоит примерно вдвое дороже.
Второй шаг — макросы, которые на части платформ выбрасывают аргументы. Их переделывают так, чтобы выражение оставалось видимым компилятору везде:
// было: на десктопе аргумент исчезал вместе с ошибкой
#define PROF_ALLOC_SCOPE(expr)
// стало: выражение не вычисляется, но имена в нём обязаны существовать
#define PROF_ALLOC_SCOPE(expr) ((void)sizeof((expr), 0))
В движке нашлось около сорока таких макросов, и примерно в пяти уже лежали вызовы несуществующих функций. Два из них оставили люди лет пять назад.
Против перепутанного порядка аргументов, когда int молча становится float, а bool индексом массива, работает -Wconversion и аналоги. На старом коде он даёт тысячи предупреждений, поэтому его включают только для файлов, которые трогает PR. Новый код сразу пишется аккуратно, старый чистится по мере правок.
Имена, которых вчера не было
Из добавленных строк диффа извлекаются все идентификаторы и сверяются с ночным индексом репозитория и third_party:
added = identifiers_from_diff(diff, only_added=True)
declared = declarations_in_diff(diff)
known = load_index("symbols.idx") # собирается ночью по всему репо и third_party
for name in sorted(added - declared - known):
comment(pr, f"{name}: не нашел ни объявления, ни использования")
Компилятор не видит выдуманное имя в трёх местах: в коде под #ifdef для конфигураций, которые PR-сборка не покрывает; в шаблонах, которые ещё никто не инстанцировал; в строковых литералах.
Цена — ложные срабатывания. Имена, склеенные через ## (например, DECLARE_COMPONENT(Health) порождает HealthComponent и HealthComponentPool), индекс сначала не видел, и первые две недели бот ругался на каждый второй PR. Помогло построение индекса по препроцессированным исходникам: плюс три минуты к ночной сборке и около 2 ГБ на диске. Плюс файл исключений, который растёт быстрее, чем его читают.
Ключи конфигов и другие строки
Модель видит чтение render.shadows.cascade_count и дописывает соседнее:
float bias = Cfg::GetFloat("render.shadows.cascade_bias", 0.0f);
Ключа нет, геттер возвращает ноль, тени слегка отлипают от стен, и замечают это через три недели. То же происходит с консольными переменными, игровыми событиями, Lua-биндингами, параметрами шейдеров и путями к ассетам.
Решение: при ночной сборке генерируется схема из всех конфигов, Lua-биндингов и манифестов ассетов. Проверка находит в диффе вызовы Cfg::Get*, CVar::Find, Events::Subscribe со строковым литералом и сверяет литерал со схемой. Хватает регулярки на 15 строк: она находит около 90% таких вызовов. Новые геттеры конфигов сделаны без значения по умолчанию, и при отсутствии ключа дев-сборка падает на старте.
Пакеты, которых нет
В Python-скриптах пайплайна ассетов, плагинах редактора и веб-тулах модель приносит несуществующие пакеты или пакеты с похожими именами. Однажды в requirements приехал пакет, отличавшийся от настоящего одной буквой, с единственным релизом трёхнедельной давности. Спасло внутреннее зеркало: сборочные машины ходят только в него, пакеты туда добавляют руками, и CI просто упал.
Теперь любое изменение в vcpkg.json, conanfile, requirements и package.json требует апрува билд-команды. До апрува CI проверяет, что новый пакет есть в зеркале, у него больше одного релиза и он старше пары месяцев.
Подозрительные заплатки и пустые тесты
На просьбу «почини тест» модель ищет кратчайший путь к зелёной галочке. Бот ищет в добавленных строках pragma с отключением предупреждений, NOLINT, const_cast, memory_order_relaxed, префикс DISABLED_ в тестах, изменённые ожидаемые значения в существующих тестах и новые ранние return по nullptr. PR это не блокирует, но добавляет обязательного ревьювера из владельцев модуля. Чаще всего попадаются как раз ранние возвраты, превращающие громкий краш в тихий баг, и подгонка ожиданий в тестах под новую логику.
Тесты от модели часто проверяют текущее поведение кода: ожидаемые значения берутся из той же реализации. Для этого используется мутационное тестирование на Mull, только по функциям из PR, потому что полный прогон по движку идёт больше суток. На PR с сорока новыми тестами для инвентаря из примерно семидесяти мутантов тесты убили шесть. Цена — 15–25 минут на каждый PR с тестами.
Санитайзеры и детерминизм
Ошибки времени жизни модель пишет идиоматично:
std::string_view name = entity.GetDebugName(); // вернули std::string по значению
jobs.Schedule([&] { Log("%s", name.data()); }); // строка уже умерла, джоб ещё в очереди
Каждый PR собирается с ASan и UBSan для затронутых модулей и гоняет их юнит-тесты. TSan запускается ночью на стресс-тестах джоб-системы: замедление в 5–10 раз и ложные срабатывания на самописном lock-free коде на каждом PR никто терпеть не станет.
Симуляция в движке детерминированная, на ней держатся реплеи и сетевой режим. Модель переставляет слагаемые, заменяет детерминированные sin/sqrt на стандартные, вставляет std::fma, итерируется по unordered_map. Через пару тысяч тиков состояния на двух машинах расходятся. В CI лежит около тридцати сессий по 5–10 минут игрового времени; на PR, затрагивающем симуляцию, они проигрываются с хешем состояния на каждом тике, бот сообщает первый разошедшийся тик и обновлявшиеся системы. Около 18 минут плюс перезапись реплеев при каждой смене баланса.
Вторая модель в роли ревьювера
Полноценное ревью второй моделью продержалось два месяца: реальные и выдуманные проблемы она находила с одинаковой уверенностью, и люди перестали читать её комментарии. Прижилось узкое применение: модель получает описание PR и дифф и пишет только то, что дифф делает сверх описанного. Комментарий вида «кроме заявленного, изменён порог видимости в AISensing.cpp» читается за пять секунд и дважды сэкономил по неделе отладки.
Последнее и самое дешёвое — галочка в описании PR «писалось с моделью». Такие PR обязательно смотрит владелец модуля, даже при зелёных проверках.
Время и деньги
| Шаг | Время |
|---|---|
| Было: сборка PC + юнит-тесты | ~14 мин |
| Все тулчейны параллельно | ~22 мин |
| Индекс имён и схема конфигов | ~2 мин |
| ASan/UBSan по затронутым модулям | ~12 мин |
| Реплеи (если тронута симуляция) | ~18 мин |
| Мутации (если в PR есть тесты) | ~15–25 мин |
| Машинное время на типичный PR | ~60–80 мин |
| Ожидание человека | ~40 мин |
При 60 PR в день это около 50 лишних часов машинного времени ежедневно и половина ставки билд-инженера. По грубой оценке выигрыш от моделей остаётся положительным, но он меньше, чем на презентациях, где ускорение меряют до первого коммита.
Что пропускается
Градусы вместо радиан, перепутанная левая и правая система координат при импорте из DCC-пакета, правдоподобная, но неверная формула затухания урона — всё это компилируется, проходит санитайзеры и ловится только плейтестом, QA или игроками. Вторая дыра — обход проверок: имя новой функции добавляют в файл исключений вместе с ней самой, реплеи перезаписывают без объяснений, а модель просят «сделать так, чтобы бот не ругался».
Полезный побочный эффект: проверка новых имён и схема конфигов примерно в половине срабатываний ловят опечатки людей. Так нашлась пара старых багов с «иногда не применяющимися» настройками графики, так что часть стоимости пайплайна уходит на обычную гигиену кода.