Files
progcode/editorial/agent-rewrites/105.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>"
}