Зелёный тест у generated diff выглядит как готовый ответ: задача закрыта, экран отрисовался, число совпало. Цена этой спешки появляется на границе, которую тест не описал. Публичное поле меняет имя, preview меняет входной объект или пустая роль попадает в защищённую ветку. В синтетическом примере это означает второй review и задержку решения; в реальном проекте стоимость зависит от контракта, данных и полномочий, поэтому здесь её не будем придумывать.
Причина не в том, что generated code обязательно плох. Его опасно принимать как доказательство. Ответ ассистента, один линтер и один happy-path test отвечают на разные вопросы, а иногда на один и тот же. Проверка начинается с явного плана: какой риск виден, какое свидетельство его ищет, где свидетельство заканчивается и кто вправе принять остаточную неопределённость.
Сначала зафиксировать границу diff
Перед запуском проверки я записываю одну строку контракта. Для mapper это вход, выход и запрет на переименование поля. Для preview — кто владеет входным объектом и разрешена ли мутация. Для guard — допустимые роли и default-deny, то есть отказ при неизвестном значении. Эта строка дешевле полной спецификации, но она уже позволяет увидеть, что «возвращает число» не равняется «сохраняет API».
Дальше diff получает не общий запрос «проверь код», а пять коротких вопросов. Контракт проверяет форму и правило. Static check ищет известный паттерн. Focused test запускает один ожидаемый и один отрицательный путь. Human review сравнивает намерение, условие и побочный эффект. Manual reproduction повторяет маленький сценарий глазами потребителя. Это не пять ступеней гарантии: каждый шаг оставляет слепую зону.
Verification plan до проверки, а не после зелёного статуса
| Свидетельство | Какой вопрос задаёт | Дешёвый результат | Чего не доказывает |
|---|---|---|---|
| Contract | Сохранилась ли форма и запрет? | поле, инвариант или decision записаны явно | полноту всех consumers и безопасность всей системы |
| Static check | Есть ли известный опасный паттерн? | найдено прямое переименование или присваивание | семантику каждого вызова и runtime effect |
| Focused test | Работает ли конкретный accept/reject path? | позитивный и негативный expected result | покрытие всех комбинаций и угроз |
| Human review | Совпадают ли intent, code и граница? | объяснённый verdict и вопрос владельцу | отсутствие всех ошибок в большом diff |
| Manual reproduction | Видит ли потребитель заявленный результат? | короткая трасса fixed input → output | поведение production и реальных пользователей |
Такой plan помогает сравнить стоимость вариантов. Полный прогон широкого набора тестов полезен, когда изменение реально затрагивает много границ, но он дороже по времени и всё равно может не проверить форму публичного результата. Узкий contract + negative test + review дешевле для одного маленького diff, потому что быстрее показывает именно заявленный риск. Он не заменяет широкую проверку, если scope уже вырос. Выбор надо привязать к границе изменения, а не к уверенности генератора.
Компактный fixed synthetic example
Ниже не prompt, не model call и не код из репозитория. Это фиксированная учебная карточка из overlay. Она строит report, затем plan и намеренно останавливает merge preparation: в карточке доказательства расходятся. Пример можно запустить в памяти Node.js; он не читает файлы, Git, CI, сеть, telemetry или production.
import {
createFixedSyntheticVerificationInput,
inspectSyntheticGeneratedDiff,
buildSyntheticVerificationPlan,
stopSyntheticMerge,
} from './upgrade-2025-02.mjs';
const input = createFixedSyntheticVerificationInput('contract-mismatch');
const report = inspectSyntheticGeneratedDiff(input);
const plan = buildSyntheticVerificationPlan(report);
const stopped = stopSyntheticMerge(plan);
console.log(report.verdict); // evidence-disagrees-stop-before-human-approval
console.log(plan.mergeDecision); // blocked-until-independent-evidence-agrees
console.log(stopped.effect); // no-system-change
В case contract-mismatch generated mapper возвращает total, а fixed contract требует amountCents. Happy-path test проверяет, что получилось число, поэтому остаётся зелёным. Contract check, consumer-oriented test и review задают другой вопрос: доступно ли поле, на которое рассчитывает потребитель? Их расхождение — не повод подобрать тест до зелёного результата. Это повод оставить merge blocked и сначала решить, требуется ли совместимость или отдельный change decision.
Пять шагов для одного небольшого изменения
- Сузьте scope. Назовите один файл, один contract boundary и один ожидаемый эффект. Если это не получается, diff уже слишком широк для короткого review.
- Запишите negative path. Рядом с accept case добавьте отказ, отсутствие значения, старое поле или запретный side effect. Один happy path не выбирает отрицательную ветку за вас.
- Разделите output и state. Для preview и mapper сравните не только result, но и вход после вызова. Для guard отделите «вернул 200» от «правило доступа записано верно».
- Попросите review о решении, а не о красоте diff. Reviewer должен суметь назвать contract, residual risk и owner, который может принять исключение.
- Остановите спорный merge. Если contract, test и review говорят разное, следующая работа — объяснить расхождение, а не добрать ещё один зелёный запуск.
Почему линтер и тест не складываются в гарантию
Линтер полезен для правила, которое можно выразить как паттерн. Он может показать прямое присваивание аргументу или запрещённое имя. Он не знает, разрешена ли мутация именно в этом API и не видит договор с consumers. Focused test полезен, когда ожидание написано в форме входа и результата. Он не знает про путь, который тестировавший не назвал. Review полезен для контекста, но зависит от размера diff, ясности требования и времени человека. Поэтому формулировка «прошёл линтер и тест» должна означать только это, не «корректен» и тем более не «безопасен».
GitHub в зафиксированной документации на февраль 2025 прямо описывает AI review как дополнение к human review и предупреждает о ложных срабатываниях, пропусках и небезопасных suggestions. NIST SSDF PW.7 также не выбирает единственный инструмент: он связывает review, analysis, фиксацию findings и их triage с правилами организации. Эти источники поддерживают дисциплину нескольких доказательств, но не подтверждают наши synthetic cases и не дают универсальный threshold для merge.
Stop condition и стоимость задержки
Практический stop condition короткий: остановить подготовку merge, когда заявленный contract, отрицательный тест и reviewer rationale не совпадают. Это не наказание за generated code. Это экономия на более дорогом цикле: после merge команда будет выяснять, является ли отсутствующее значение новым API, допустимой мутацией или пропущенной политикой доступа. Остановить маленький diff обычно дешевле, чем расследовать неявную границу после того, как его уже приняли.
Но stop не равен rollback. Revert отбрасывает ещё не принятый change или создаёт обратный change в конкретной VCS-политике. Rollback меняет уже доставленное состояние и требует подтверждённых условий восстановления. Этот пакет не делает ни того ни другого: он только возвращает teaching plan к fixed contract и просит human approval вне модели. В реальном проекте владелец должен решить, кто имеет право на merge, revert и rollback отдельно.
Ограничения и следующий проверяемый шаг
Все diff, contracts, tests, verdicts, роли, поля и измерения в этой статье — versioned fixed synthetic literals. Они не являются наблюдением за моделью, репозиторием, пользователями, секретами, CI или production. Нельзя по ним заключать, что конкретный линтер, тест или reviewer обнаружит такой же риск в другом языке. Нельзя подменять ими threat model, policy доступа или анализ фактической зависимости.
Следующий шаг: возьмите один маленький generated diff и до запуска напишите таблицу из пяти строк: contract, static pattern, positive/negative test, reviewer question и manual scenario. У каждой строки назовите blind spot. Если хотя бы две строки спорят, не пытайтесь получить «среднее» verdict; вынесите вопрос владельцу границы. Ожидаемый результат — не больше тестов вообще, а доказательство, которое можно повторить и оспорить.
Историческая граница февраля 2025
В тексте использованы GitHub Docs на immutable commit от 12 декабря 2024, NIST SP 800-218 Version 1.1 от 3 февраля 2022 и OWASP Code Review Guide 2.0 от июля 2017. Они были доступны к февралю 2025. Более поздние сведения о моделях, агентных режимах, benchmark или возможностях инструментов сюда не переносятся.
Проверяемые источники
- GitHub Docs: Responsible use of GitHub Copilot code review (immutable GitHub Docs commit 7b3918e77baf865d1f16bd60e570acea874ee9eb, 12 December 2024) — В документе сказано, что review дополняет, а не заменяет внимательную проверку человеком; подсказка может быть неточной, синтаксически или семантически неверной либо небезопасной. Ограничение: Это документация конкретного preview-инструмента на зафиксированной ревизии. Она не измеряет качество любого другого ассистента и не даёт правила merge для конкретной команды.
- NIST SP 800-218, Secure Software Development Framework Version 1.1 (NIST SP 800-218 Version 1.1, final 3 February 2022) — PW.7 предлагает выбирать review и/или code analysis по стадии работы, проводить их по secure-coding standard и фиксировать найденные вопросы и рекомендации; результаты тестирования могут быть входом peer review. Ограничение: SSDF задаёт высокоуровневую рамку. Он не выбирает линтер, не доказывает покрытие, не определяет риск конкретного diff и не отменяет локальные полномочия.
- OWASP Code Review Guide 2.0 (OWASP Code Review Guide 2.0, July 2017 release PDF) — Руководство описывает code review как проверку присутствия и корректного вызова security и logical controls; инструменты полезны для масштаба, но контекст и подтверждение результата остаются задачей человека. Ограничение: Это общее руководство по secure review, а не каталог правил для языка, фреймворка или модели. Оно не обещает, что review найдёт все уязвимости.