{ "index": 104, "slug": "editorial-2025-02-mechanism-ai-code-verification", "title": "Как проверить сгенерированный код, когда зелёных сигналов недостаточно", "excerpt": "Сгенерированный diff может пройти линтер и happy path, но нарушить контракт, изменить чужое состояние или пропустить отрицательную ветку. Разбираем, как связать риск с независимой проверкой и когда остановить merge.", "contentHtml": "
Сгенерированный diff может пройти линтер и unit test, но сломать следующего потребителя. Mapper вернул число, хотя API требует поле amountCents. Функция preview показала правильную строку, но изменила объект, который передал вызывающий код. Проверка роли пропустила редактора, но не проверила отсутствие роли.
Цена ошибки начинается не с плохого ответа модели. Она начинается с ложной уверенности. Команда видит несколько зелёных статусов, считает риск закрытым и узнаёт о нарушенной границе в следующем consumer, review или релизном сценарии. По числу зелёных индикаторов нельзя вывести размер ущерба. Но можно заранее не принять их за одно доказательство.
\nТезис статьи простой: проверка должна связывать наблюдаемый риск с конкретным свидетельством. Контракт проверяет форму и инварианты. Статический анализ ищет формальный паттерн. Узкий тест проверяет названную ветку. Человек проверяет намерение и контекст. Ручной сценарий смотрит на путь потребителя. Эти способы пересекаются, но не заменяют друг друга.
\nНачните не с инструмента, а с разрыва. «AI ошибся» не помогает выбрать проверку. «Публичное поле переименовано», «входной объект изменился после вызова» и «пустая роль получила доступ» уже задают наблюдаемые вопросы.
\nЗатем выберите свидетельство, которое может опровергнуть именно этот разрыв. Для формы результата подойдёт assertion на схему и тест реального consumer. Для ownership нужен снимок входа до и после вызова. Для доступа нужен allow-list и отрицательный тест для неизвестного значения. Если check не способен увидеть риск, его зелёный результат ничего о риске не говорит.
\nПоследняя часть — граница. Контракт не доказывает безопасность всей системы. Линтер не понимает смысл каждого вызова. Тест не проверяет ветку, которую в него не внесли. Review зависит от контекста и внимания. Ручное воспроизведение не становится регрессионным тестом само по себе.
\nНиже приведены фиксированные учебные случаи. Они не читают репозиторий, не вызывают модель и не показывают результат реального проекта. Их задача — показать форму рассуждения: наблюдение, причина, проверка и действие.
\nКонтракт mapper требует объект { amountCents: number }. Сгенерированный код складывает subtotalCents и taxCents, но возвращает { total: 1234 }. Happy-path test проверяет только значение суммы. Он зелёный: арифметика правильная.
Потребитель читает result.amountCents и получает undefined. Симптом — зелёный тест рядом с неверной формой результата. Причина — тест описывает значение, а не публичный shape. Проверка — сравнить assertion контракта, тест consumer и вопрос reviewer: «Переименование поля принято отдельно?». Действие — вернуть amountCents или оформить совместимость как отдельное решение. Нельзя переписать тест на total только ради зелёного статуса.
Helper с именем preview получает объект заявки и возвращает правильный текст. Внутри он выполняет draft.status = 'normalized'. Если объект принадлежит caller, после preview следующий код видит изменённое состояние. Результат на экране правильный, но операция имеет побочный эффект.
function preview(draft) {\\n draft.status = 'normalized'; // скрыто меняет объект caller\\n return render(draft);\\n}\\n\\nconst draft = { status: 'raw' };\\nconst text = preview(draft);\\n\\n// text выглядит правильно, но draft.status уже равен 'normalized'\nСимптом — output совпал, а state изменился. Причина — контракт не разделяет производное значение и владение входом. Проверка — сравнить вход до и после вызова и отдельно просмотреть записи в аргумент. Действие — создать производный объект, оставить аргумент неизменным или назвать мутацию отдельной операцией. Проверка результата строки здесь недостаточна.
\nУчебный контракт разрешает роль editor и отклоняет viewer и отсутствие роли. Условие actorRole !== 'viewer' пропускает редактора, отклоняет viewer, но также пропускает undefined. Если suite содержит только тест редактора, она доказывает один accept path и ничего не говорит о неизвестном значении.
Это не доказанная уязвимость реальной авторизации: в примере нет токенов, tenant boundary, identity provider и threat model. Но конкретный контракт уже нарушен. Проверка должна включить viewer и missing-role, а reviewer должен прочитать условие как allow-list. Для доступа отрицательная ветка — часть правила, а не дополнительный тест «на всякий случай».
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Тест проверяет число, consumer не находит поле | Значение приняли за форму API | Contract assertion и consumer test | Сохранить поле или принять отдельное compatibility decision |
| Preview возвращает правильный текст, вход изменился | Не определено владение аргументом | Сравнить input до/после и найти запись в аргумент | Вернуть derived value без мутации или назвать мутацию явно |
| Редактор проходит, пустая роль тоже | Accept-case заменил allow-list | Проверить viewer и missing-role | Явно разрешить роли и отклонять неизвестные |
| Линтер зелёный, риск не назван | Для риска нет формального правила | Сверить scope правила с contract boundary | Добавить assertion, тест или вопрос reviewer |
Два свидетельства независимы не потому, что их назвали разными словами. Они независимы, когда могут опровергнуть разные предпосылки. Contract check и consumer test частично пересекаются: один смотрит на declared shape, другой — на использование поля. Это полезное пересечение. Ошибка в одном тесте не должна автоматически скрыть ошибку в другом.
\nОпаснее повторить одну неверную модель. Сгенерированный код и сгенерированный тест могут вместе решить, что отсутствие роли означает «не viewer». Второй зелёный статус тогда только усиливает доверие к ошибке. Добавьте проверку, которая получает другое основание: allow-list, внешний контракт или вопрос владельцу политики.
\nНе нужно складывать статусы в процент корректности. Static check быстро ловит формальный паттерн, но требует правила. Focused test делает один сценарий воспроизводимым, но не видит неназванную ветку. Human review замечает скрытую границу, но не заменяет точный expected result. Manual reproduction показывает путь потребителя, но плохо масштабируется. Выбирайте самый дешёвый способ, который способен оспорить текущую гипотезу.
\nВ этом примере report собирает пять видов свидетельств. При расхождении он не превращает результат в «почти готово». Он возвращает решение остановить подготовку merge до выяснения контракта.
\nconst report = inspectGeneratedDiff({\\n risk: 'public-field-renamed',\\n contract: { amountCents: 'number' },\\n result: { total: 1234 },\\n checks: ['static', 'focused-test', 'review', 'manual']\\n});\\n\\nif (report.contractAgrees === false) {\\n return { merge: 'blocked', reason: 'evidence-disagrees' };\\n}\nКод выше — учебная схема, а не готовый пакет и не production-рецепт. Она показывает важное свойство: решение опирается на конкретное расхождение, а не на число пройденных команд. В реальном проекте названия полей, правила и тестовые входы должны соответствовать фактическому контракту.
\nЭта схема не доказывает корректность кода, безопасность, отсутствие дефектов или готовность релиза. Она не заменяет threat model, анализ зависимостей, интеграционные тесты, наблюдение и правила доступа. Один учебный отрицательный тест не покрывает все реальные роли и переходы состояний.
\nStop означает только «не продолжать подготовку merge при расхождении evidence». Он не означает revert или rollback. Revert требует решения о конкретном изменении в истории VCS. Rollback относится к уже доставленному состоянию и требует подтверждённого пути восстановления. Не подменяйте отсутствие решения словом rollback.
\nИсточники тоже не дают готового verdict. Они поддерживают практику, но не выбирают порог для вашего репозитория. GitHub описывает Copilot code review как комментарий, который не заменяет required approval. NIST связывает review и analysis с процессом разработки и triage findings. OWASP подчёркивает, что автоматизированные инструменты дополняют ручной анализ там, где нужен бизнес-контекст.
\nDiff готов к передаче на human approval только тогда, когда для каждого заявленного риска записаны scope, прямое свидетельство, отрицательный путь и граница того, чего проверка не доказывает. Contract, test и reviewer rationale не должны противоречить друг другу. Если противоречие осталось, готовый результат — не зелёный статус, а понятный stop с вопросом владельцу.
" }