8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 105,
|
||
"slug": "editorial-2025-02-practice-ai-code-verification",
|
||
"title": "Зелёный тест не доказывает корректность AI-изменения",
|
||
"excerpt": "Как проверить сгенерированный diff по контракту, отрицательной ветке, побочным эффектам и независимому review — и когда остановить merge.",
|
||
"contentHtml": "<p>Сгенерированный diff может пройти линтер и один unit-тест, но сломать потребителя. Типичный симптом — тест проверяет значение, а публичный контракт требует другое имя поля. Другой симптом — preview возвращает правильный текст, но меняет входной объект. Третий — роль editor проходит, а пустая роль тоже получает доступ. Цена ошибки начинается с повторного разбора и задержки merge. В реальной системе она может включать неверные данные, отказ функции или нарушение политики доступа. По одному зелёному статусу цену не определить.</p>\n<p>Тезис простой: результат AI-инструмента — это материал для проверки, а не доказательство. Корректность появляется только тогда, когда заявленный контракт, наблюдаемое поведение и решение ревьюера совпадают. Каждый сигнал должен отвечать на свой вопрос. Тест проверяет названный сценарий. Статический анализ ищет известный паттерн. Review проверяет намерение и границы. Ручное воспроизведение смотрит на путь потребителя.</p>\n<h2>Сначала назовите границу изменения</h2>\n<p>До запуска тестов запишите, что именно нельзя нарушить. Для mapper это форма результата и имена полей. Для функции preview — владение входным объектом и запрет на скрытую мутацию. Для проверки роли — список разрешённых значений и поведение при отсутствии роли. Для обработчика ошибки — статус, формат ответа и отсутствие утечки деталей.</p>\n<p>Фраза «функция должна вернуть итог» слишком широкая. Рабочая формулировка выглядит так: «при входе с <code>subtotalCents</code> и <code>taxCents</code> вернуть объект <code>{ amountCents: number }</code>; вход не менять». В ней есть вход, выход и инвариант. По ней можно написать проверку и понять, где заканчивается её область действия.</p>\n<pre><code>contract = {\n input: { subtotalCents: 900, taxCents: 100 },\n output: { amountCents: 1000 },\n invariant: 'input remains unchanged'\n}</code></pre>\n<p>Этот фрагмент — учебный пример. Он не обращается к репозиторию, базе, CI или production. Его задача — показать форму контракта, а не предсказать поведение конкретной модели.</p>\n<figure><img src=\"/assets/editorial/2025/ai-code-verification-2025-verification-funnel.svg\" alt=\"Воронка проверки AI-изменения: контракт, статический анализ, узкий тест, review и ручное воспроизведение\"><figcaption>Проверка сужает вопрос по шагам. Ни один шаг не превращается в универсальную гарантию.</figcaption></figure>\n<h2>Как один зелёный тест пропускает ошибку</h2>\n<p>Представим учебную функцию, которую изменил ассистент:</p>\n<pre><code>function toInvoice(input) {\n return {\n total: input.subtotalCents + input.taxCents\n };\n}\n\nconst result = toInvoice({ subtotalCents: 900, taxCents: 100 });\nconsole.assert(result.total === 1000);</code></pre>\n<p>Тест зелёный. Арифметика верна. Но контракт требует <code>amountCents</code>, а consumer читает <code>result.amountCents</code>. Значение становится <code>undefined</code>. Ошибка возникла не в сложной математике. Тест задал слишком узкий вопрос и не проверил форму публичного результата.</p>\n<p>Исправленная проверка должна смотреть на поле, доступное потребителю:</p>\n<pre><code>const input = { subtotalCents: 900, taxCents: 100 };\nconst result = toInvoice(input);\n\nconsole.assert(result.amountCents === 1000);\nconsole.assert(!('total' in result));\nconsole.assert(input.subtotalCents === 900);\nconsole.assert(input.taxCents === 100);</code></pre>\n<p>Второй пример тоже учебный. Он не доказывает безопасность функции и не заменяет тесты всех реальных потребителей. Он показывает, как превратить контракт в наблюдаемое утверждение.</p>\n<h2>Симптомы и действия</h2>\n<table><caption>Диагностика небольшого AI-generated diff</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Тест проверяет число, consumer не находит поле</td><td>Проверили значение, но не shape</td><td>Сравнить ожидаемые ключи и чтение consumer</td><td>Вернуть контрактное поле или отдельно принять изменение API</td></tr><tr><td>Preview выдаёт верный текст, вход изменился</td><td>Не зафиксировано владение input</td><td>Сравнить объект до и после вызова</td><td>Создать derived value или назвать мутацию отдельной операцией</td></tr><tr><td>Editor проходит, missing role тоже проходит</td><td>Есть accept case, но нет default-deny</td><td>Проверить viewer и отсутствие значения</td><td>Записать allow-list и добавить отрицательные тесты</td></tr><tr><td>Линтер чистый, смысл изменения неясен</td><td>Паттерн проверен вместо намерения</td><td>Сопоставить diff с контрактом и owner boundary</td><td>Уменьшить scope и запросить предметный review</td></tr><tr><td>Ручной сценарий расходится с unit-тестом</td><td>Тест не повторяет путь потребителя</td><td>Воспроизвести тот же вход от API или UI до результата</td><td>Остановить merge до объяснения расхождения</td></tr></tbody></table>\n<h2>Почему независимые проверки не складываются в одну гарантию</h2>\n<p>Статический анализ видит то, для чего у него есть правило. Он может найти присваивание аргументу или подозрительный вызов. Он не знает, разрешена ли мутация в конкретном API. Unit-тест видит только входы и ожидания, которые записал автор. Он не проверяет незаписанный путь. Review видит контекст, но зависит от размера diff, ясности требования и времени ревьюера. Ручная проверка видит один маршрут пользователя и может не охватить редкий вариант.</p>\n<p>Поэтому пять зелёных сигналов могут повторять одну предпосылку. Например, линтер, unit-тест и screenshot подтверждают, что экран получил число. Ни один из них не подтверждает, что имя публичного поля осталось прежним. Независимость означает не количество инструментов, а разные вопросы и разные способы обнаружить нарушение.</p>\n<p>Нужно заранее написать blind spot каждого шага. Контракт не доказывает реализацию. Тест не доказывает покрытие всех consumers. Review не доказывает отсутствие runtime-ошибки. Такой список не делает систему безопасной сам по себе. Он не даёт зелёному статусу большего смысла, чем тот, который реально проверен.</p>\n<h2>Порядок проверки одного небольшого diff</h2>\n<ol><li><strong>Сузьте scope.</strong> Назовите изменённый файл, одну границу и один ожидаемый эффект. Если описание требует нескольких страниц, diff уже не подходит для короткой проверки.</li><li><strong>Запишите контракт.</strong> Укажите вход, выход, запрещённый side effect и условие отказа. Не заменяйте их словами «работает правильно».</li><li><strong>Добавьте отрицательный путь.</strong> Проверьте старое поле, отсутствие значения, чужую роль, неверный формат или отказ внешней зависимости — тот случай, который реально относится к границе.</li><li><strong>Сравните output и state.</strong> Для mapper проверьте shape. Для preview сравните input до и после. Для access rule проверьте не только ответ 200, но и условие допуска.</li><li><strong>Проведите предметный review.</strong> Ревьюер должен назвать контракт, остаточный риск и владельца решения. Просьба «посмотреть AI-код» не задаёт проверяемый вопрос.</li><li><strong>Повторите короткий маршрут потребителя.</strong> Используйте фиксированный учебный вход или безопасный тестовый объект. Сопоставьте результат с тем, что увидит consumer.</li><li><strong>Остановите подготовку merge при расхождении.</strong> Не подгоняйте тест под код и не добавляйте случайный запуск ради зелёного статуса. Сначала объясните конфликт или измените контракт явно.</li></ol>\n<h2>Отрицательный путь важнее ещё одного happy path</h2>\n<p>Отрицательная ветка должна следовать из контракта. Если неизвестная роль не должна получать доступ, проверяйте и <code>viewer</code>, и отсутствие роли. Если preview не меняет вход, проверяйте равенство объекта после вызова. Если поле нельзя переименовывать без совместимости, проверяйте ключ результата и чтение старого consumer.</p>\n<pre><code>function canEdit(actorRole) {\n return actorRole === 'editor';\n}\n\nconsole.assert(canEdit('editor') === true);\nconsole.assert(canEdit('viewer') === false);\nconsole.assert(canEdit(undefined) === false);</code></pre>\n<p>Это не threat model и не доказательство безопасности авторизации. В примере нет identity provider, tenant boundary, токена или реального хранилища прав. Он проверяет только заявленное правило для трёх фиксированных входов. Если production-контракт шире, пример нужно расширить фактическими условиями, а не переносить его verdict.</p>\n<h2>Когда нужно остановиться</h2>\n<p>Практический stop condition таков: contract, отрицательный тест и rationale ревьюера описывают разные результаты. Тогда merge preparation блокируется. Это не автоматический revert и не rollback. Ничего доставленного система не меняет. Команда только прекращает движение спорного diff, пока владелец границы не выберет одно из действий: вернуть код к контракту, оформить совместимое изменение или собрать недостающее свидетельство.</p>\n<p>Если расхождение нельзя объяснить одним предложением, не передавайте diff на approval. Approval должен отвечать на конкретный вопрос: можно ли принять переименование поля, кто владеет совместимостью, почему мутация разрешена или какое правило действует для отсутствующей роли. Человек утверждает осознанное исключение, а не пустоту в проверке.</p>\n<h2>Ограничения</h2>\n<p>AI-проверка не заменяет знание домена, threat model, тесты реальных интеграций и ответственность владельца. Ассистент может придумать несуществующий API, пропустить условие, удалить падающий тест или предложить зависимость с неподходящей лицензией. Статический инструмент может не понимать бизнес-правило. Ручной review тоже может пропустить ошибку.</p>\n<p>Все функции и данные в примерах вымышлены и предназначены только для обучения. В статье нет production-измерений, утверждений о качестве конкретной модели и обещания, что описанный набор шагов обнаружит любой дефект. Перед применением нужно заменить учебный контракт фактическим API, входами, ролями и условиями доставки.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Небольшой diff готов к следующему решению, когда выполнены четыре условия: контракт записан; позитивный и отрицательный сценарии проходят на одних и тех же входных данных; побочный эффект либо запрещён и проверен, либо назван частью контракта; ревьюер может объяснить, что проверено и что осталось вне scope. Если хотя бы одно условие не выполнено, статус «зелёный» описывает запуск инструмента, а не готовность изменения.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.github.com/en/copilot/tutorials/review-ai-generated-code\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: Review AI-generated code</a> — функциональные проверки, контекст, зависимости, AI-специфические ошибки и совместный review.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218 SSDF</a> — практики безопасной разработки, проверки и работы с найденными проблемами.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Secure_Code_Review_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Secure Code Review Cheat Sheet</a> — граница между автоматизированным анализом и ручной проверкой контекста.</li></ul>"
|
||
}
|