8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"index": 106,
|
||
"slug": "editorial-2025-01-field-ai-coding-assistant",
|
||
"title": "AI-помощник в задаче: как проверить diff перед merge",
|
||
"excerpt": "Сгенерированный код может пройти happy path и всё равно выйти за границу задачи, изменить контракт или добавить непроверенную зависимость. Разбираем практический gate перед merge: scope, отрицательные ветки, evidence и ограничения человеческого решения.",
|
||
"contentHtml": "<p>На ревью приходит небольшой diff, который подготовил AI-помощник. Основной тест зелёный, названия аккуратные, объяснение звучит уверенно. При чтении полного изменения обнаруживается второй файл с правами доступа, новый пакет в lock-файле или ветка ошибки, в которой запись выполняется до проверки входа. Такой diff выглядит локальным только в окне редактора. Цена пропуска — сломанный контракт, утечка данных или откат, который придётся готовить уже после merge.</p>\n<p>Разберём учебный сценарий: помощник должен нормализовать входящее событие, но предложил изменить ещё и вызывающий код. Главный вывод не зависит от конкретного инструмента: ответ модели — кандидат на изменение, а не доказательство корректности. Перед merge человек должен связать каждый изменённый путь с задачей, каждую изменённую ветку — с контрактом, а каждое существенное утверждение — с воспроизводимой проверкой.</p>\n<h2>Что считается готовым diff</h2>\n<p>Готовность здесь означает не «код выглядит правильно», а возможность показать короткую цепочку evidence. Сначала есть формулировка задачи и список разрешённых областей. Затем — diff, который не вышел за эти области. Для изменённого поведения записаны вход, ожидаемый результат и запрещённый побочный эффект. Наконец, тест или другой запуск действительно проходит через эту ветку в окружении, которое указано в результате проверки.</p>\n<p>У gate есть три исхода. <code>approve</code> означает, что reviewer готов принять конкретный diff. <code>request-changes</code> означает, что проблему можно исправить в той же задаче. <code>stop</code> означает, что обнаружена неизвестная граница, security-sensitive изменение или несоответствие контракта; такой случай сначала возвращают владельцу риска.</p>\n<figure><img src=\"/assets/editorial/2025/ai-coding-assistant-2025-verification-gate.svg\" alt=\"Схема проверки сгенерированного diff: карточка задачи, ограниченный diff, строки контракта и целевой тест сходятся в gate; несоответствие ведёт к остановке, а решение принимает человек\" loading=\"lazy\" /><figcaption>Gate не оценивает убедительность ответа модели. Он сопоставляет scope, контракт и evidence. Красная ветка означает остановку до merge; схема не запускает CI и не доказывает безопасность production-системы.</figcaption></figure>\n<h2>Сначала проверьте границу изменения</h2>\n<p>Первый вопрос к diff — не «хорошо ли написан код», а «имеет ли он право здесь находиться». Для pull request можно получить список изменённых путей и отдельную проверку пробелов или конфликтных маркеров:</p>\n<pre><code>git diff --name-status origin/main...HEAD\ngit diff --check origin/main...HEAD\ngit diff --stat origin/main...HEAD</code></pre>\n<p>Эти команды показывают состав изменения и базовые технические дефекты, но не знают смысла задачи. Сопоставьте каждый путь с карточкой: исходный модуль, тест, схема, миграция, lock-файл и конфигурация — это разные виды риска. Если помощник добавил пакет «для удобства», изменил генератор или затронул авторизацию, не прячьте это в общий diff. Уточните границу либо вынесите изменение в отдельную задачу с отдельным владельцем.</p>\n<p>Особенно легко пропустить сгенерированные файлы. Они могут появиться после команды сборки, хотя не были частью замысла. Проверьте статус репозитория до и после запуска инструментов, а также правила, по которым артефакты попадают в commit. Для rename и удаления смотрите не только имя нового файла: контракт мог переехать, а тест — остаться привязанным к старому пути.</p>\n<h2>Затем разложите контракт по веткам</h2>\n<p>Небольшая функция часто имеет больше одного смысла входа. В учебном обработчике события есть валидное сообщение, отсутствие обязательного поля и неизвестный статус. Если все три случая свести к пустому объекту, вызывающий код может принять ошибку за отсутствие данных и продолжить обработку. Это не косметическая разница: меняется решение на границе системы.</p>\n<div class=\"table-scroll\"><table><caption>Минимальная матрица проверки для обработчика события</caption><thead><tr><th scope=\"col\">Вход</th><th scope=\"col\">Ожидаемый результат</th><th scope=\"col\">Запрещённый эффект</th><th scope=\"col\">Evidence</th></tr></thead><tbody><tr><td>Поле <code>type</code> известно, payload полный</td><td>Нормализованный объект передан дальше</td><td>Не создавать запись дважды</td><td>Unit test и проверка вызова downstream</td></tr><tr><td>Нет <code>type</code></td><td><code>invalid-input</code></td><td>Не вызывать запись и повторную доставку</td><td>Negative test с проверкой числа вызовов</td></tr><tr><td><code>type</code> неизвестен</td><td><code>unsupported-type</code></td><td>Не угадывать обработчик по похожему имени</td><td>Тест на неизвестное значение и лог причины</td></tr><tr><td>Payload содержит лишнее поле</td><td>Результат зависит от версии контракта</td><td>Не молча терять данные, если контракт их запрещает</td><td>Сверка схемы и тест сериализации</td></tr></tbody></table></div>\n<p>Таблица не заменяет спецификацию API. Она делает видимым место, где тест должен отличать результат от побочного эффекта. Проектные значения — имена статусов, политика лишних полей и способ дедупликации — нельзя переносить в другой сервис без сверки его контракта.</p>\n<h2>Положительный тест без отрицательного пути недостаточен</h2>\n<p>Минимальный воспроизводимый пример можно выполнить обычным Node.js без внешних пакетов. Он проверяет три входа и отдельно фиксирует, что downstream не вызывается при ошибке:</p>\n<pre><code>const calls = [];\n\nfunction normalizeEvent(event) {\n if (!event?.type) return { status: 'invalid-input' };\n if (event.type !== 'invoice.created') return { status: 'unsupported-type' };\n return { status: 'ok', type: event.type, invoiceId: event.invoiceId };\n}\n\nfunction handle(event) {\n const result = normalizeEvent(event);\n if (result.status !== 'ok') return result;\n calls.push(result.invoiceId); // downstream: только после проверки\n return result;\n}\n\nconsole.log(handle({ type: 'invoice.created', invoiceId: 'inv-7' }));\nconsole.log(handle({ invoiceId: 'inv-8' }));\nconsole.log(handle({ type: 'invoice.deleted', invoiceId: 'inv-9' }));\nconsole.log({ calls });\n\n// Ожидается: два отказа и calls только с ['inv-7'].</code></pre>\n<p>Запустите этот фрагмент как <code>node example.mjs</code> или вставьте его в Node REPL. Важно не само число строк, а наблюдаемый инвариант: ошибки возвращаются до побочного действия. Если сгенерированный вариант вызывает <code>calls.push</code> до проверки статуса, happy path останется зелёным, а отрицательный тест покажет нарушение.</p>\n<p>Тест должен быть связан с изменённой веткой, а не просто лежать рядом по имени. Прочитайте assertion: он проверяет значение, тип ошибки, количество вызовов и состояние после отказа? Если проверяется только непустой ответ, неизвестно, что произойдёт с записью, повторной доставкой или метрикой.</p>\n<h2>Проверьте зависимости и границы данных</h2>\n<p>AI-помощник может предложить несуществующий пакет, устаревший API или библиотеку с неподходящей лицензией. Может также перенести в prompt секрет, персональный идентификатор или фрагмент закрытого кода. Поэтому просмотр diff должен включать не только строки программы.</p>\n<ol><li><strong>Зависимости.</strong> Сверьте имя пакета с реестром и документацией, версию — с поддерживаемым runtime, а лицензию и транзитивные зависимости — с правилами проекта.</li><li><strong>Секреты.</strong> Уберите токены, cookies, персональные данные и закрытый исходный код из контекста помощника. Проверьте diff, логи и артефакты на случайное попадание таких данных.</li><li><strong>Права.</strong> Если изменение касается авторизации, фильтрации данных, SQL, файловой системы или внешнего вызова, назначьте ревьюера с соответствующей ответственностью. Статический анализ не заменяет такой review.</li><li><strong>Схема и сериализация.</strong> Сравните фактический JSON, заголовки, коды ошибок и обязательность полей с владельцем API. Компилируемый тип не доказывает совместимость с потребителем.</li><li><strong>Окружение.</strong> Запишите версии Node.js, команды и набор тестов. Результат локального запуска нельзя расширять до гарантии другой ОС, базы или feature flag.</li></ol>\n<p>GitHub в руководстве по проверке AI-generated code отдельно выделяет функциональные проверки, контекст и намерение, зависимости, AI-специфичные ошибки и человеческое ревью. Это полезная карта вопросов, но не сертификат качества конкретного diff. NIST SSDF и профиль для generative AI задают практики безопасной разработки и управления рисками; они не подтверждают, что учебный код или конкретная модель безопасны.</p>\n<h2>Что делать при несоответствии</h2>\n<p>При mismatch не исправляйте сразу весь diff новым широким запросом к помощнику. Сначала зафиксируйте наблюдение: какой path лишний, какая строка контракта нарушена, какая проверка отсутствует и какой эффект запрещён. Затем выберите узкое действие.</p>\n<ul><li>Лишний путь — удалить из текущей задачи или открыть отдельный change с новой границей.</li><li>Неверная ветка ошибки — вернуть различие статусов и добавить negative test до повторного ревью.</li><li>Непроверенная зависимость — остановить изменение, проверить пакет и согласовать лицензию.</li><li>Security-sensitive diff — передать его владельцу риска, не объявляя результат безопасным по зелёному lint.</li><li>Неизвестный consumer или runtime — собрать evidence, а до этого оставить решение в статусе <code>stop</code>.</li></ul>\n<p>Остановка не означает автоматический rollback. До merge обычно достаточно сузить кандидат и запросить изменения. После поставки нужна отдельная процедура инцидента и отката, зависящая от системы. Не смешивайте эти решения: gate отвечает на вопрос «можно ли принимать этот diff сейчас», а не «как восстановить production после уже случившегося эффекта».</p>\n<h2>Порядок проверки перед merge</h2>\n<ol><li>Сформулируйте одно решение: принять diff, запросить правки или остановить работу.</li><li>Запишите разрешённые пути, запреты, владельца задачи и чувствительные области.</li><li>Снимите <code>git diff --name-status</code>, прочитайте весь diff и проверьте незапланированные артефакты.</li><li>Для каждой изменённой ветки заполните вход, результат, статус или исключение и запрещённый эффект.</li><li>Найдите тест, который выполняет ветку, и добавьте отрицательный случай для неверного входа, отсутствующего поля или отказа в доступе.</li><li>Проверьте зависимости, схему, сериализацию, права, секреты и окружение.</li><li>Запустите заявленные тесты и <code>git diff --check</code>; сохраните команды и фактический результат.</li><li>Отдельно перечислите неизвестные. Не превращайте отсутствие evidence в pass и не расширяйте доказательство за пределы выполненного запуска.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Этот gate снижает риск пропустить очевидное несоответствие, но не доказывает полноту поиска потребителей, отсутствие уязвимостей или корректность бизнес-решения. Unit test может не увидеть реальный сериализатор, конкурентный вызов, миграцию или конфигурацию в другом окружении. Static analysis может найти подозрительный путь, но не понять, что конкретный статус запрещено менять в этом продукте.</p>\n<p>Пример с <code>invoice.created</code> синтетический: он не подключён к очереди, базе и системе прав. Значения статусов, формат идентификатора и правило дедупликации — иллюстрация, а не универсальный API-контракт. Для платежей, персональных данных, авторизации и миграций нужен профильный владелец, проверка реального окружения и принятый в проекте план отката.</p>\n<p>Не следует приписывать модели результат, которого не измеряли. Без сравнимых задач и заранее заданной метрики этот процесс не доказывает экономию времени, рост качества или отсутствие дефектов. Он лишь делает решение о конкретном diff проверяемым и оставляет явное место для остановки.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Diff можно передавать на решение о merge, когда reviewer показывает: каждый path разрешён задачей; каждая изменённая ветка связана с тестом или другим воспроизводимым evidence; valid, absent и invalid входы не смешаны; запрещённые побочные эффекты проверены; зависимости, данные и права просмотрены; команды и окружение записаны; неизвестные не выданы за доказанный факт. Если хотя бы один пункт не выполнен, корректный результат — запросить конкретную проверку или остановить merge.</p>\n<h2>Проверяемые источники</h2><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-специфичные ошибки и сохранять человеческое ревью. Руководство не подтверждает качество конкретного diff.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/a/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218A: Secure Software Development Practices for Generative AI and Dual-Use Foundation Models</a> — официальный профиль дополняет SSDF практиками для AI-моделей и AI-систем на протяжении жизненного цикла. Это рамка управления рисками, а не проверка учебного кода.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1</a> — официальный набор высокоуровневых практик secure development и общий язык для обсуждения рисков. Он не заменяет доменный контракт, тесты и review владельца системы.</li></ul>"
|
||
}
|