{ "index": 106, "slug": "editorial-2025-01-field-ai-coding-assistant", "title": "AI-помощник перед merge: как проверить diff, а не впечатление", "excerpt": "Сгенерированный diff может пройти happy path и всё равно выйти за scope, изменить контракт или не иметь теста для изменённой ветки. Разбираем проверяемый gate перед merge: граница задачи, отрицательный путь, evidence и критерий готовности.", "contentHtml": "

Перед merge лежит небольшой сгенерированный diff. Он исправляет видимый симптом, тест рядом зелёный, а код выглядит аккуратно. Через час выясняется, что правка изменила ветку отказа, добавила доступ к данным или превратила ошибочный вход в допустимый. Цена ошибки — не только откат. Нужно найти затронутый контракт, остановить выпуск, проверить уже собранные артефакты и вернуть доверие к проверке.

\n

Тезис простой: ответ AI-помощника — кандидат на изменение, а не доказательство корректности. Перед merge нужно проверить три границы: diff меняет только разрешённый scope, сохраняет контракт и имеет evidence для каждой изменённой ветки. Линтер и успешный happy path закрывают лишь часть вопросов.

\n

Что именно проверяет gate

\n

Verification gate отделяет candidate diff от решения человека. Он не одобряет pull request автоматически и не обещает безопасность. Он собирает четыре наблюдаемых поля: задачу, разрешённые пути, условия контракта и проверку результата. Если поле не заполнено, verdict должен остановиться на stop, а не превращаться в «скорее всего, всё хорошо».

\n
const review = {\n  task: 'normalize invoice key',\n  allowedPaths: ['src/invoice/normalizeKey.ts'],\n  forbiddenEffects: ['change authorization', 'write on invalid input'],\n  evidence: [\n    { input: 'valid-key', expected: 'valid-key', writes: 0 },\n    { input: 'invalid-key', expected: 'error', writes: 0 },\n  ],\n};\n\nfunction canMerge(candidate, contract) {\n  const pathsOk = candidate.paths.every((path) => contract.allowedPaths.includes(path));\n  const behaviorOk = candidate.invalidInput.writes === 0;\n  return pathsOk && behaviorOk;\n}\n\n// Учебный пример: он не запускает CI и не проверяет реальный репозиторий.\n// false означает остановку, а не разрешение исправить scope молча.
\n

В примере canMerge показывает только форму решения. Реальный проект должен связать каждое условие с тестом, ревьюером и фактическим diff. Нельзя считать этот фрагмент защитой доступа, транзакций или всех потребителей API.

\n

Три способа принять неверный diff

\n

Scope mismatch

\n

Задача просит нормализовать ключ счёта. Помощник меняет parser и соседний accessDecision. Изменение авторизации может быть технически небольшим, но его риск не равен риску форматирования строки. Если path не назван в задаче, его нельзя включать в тот же merge под видом удобного сопутствующего исправления.

\n

Правильный отрицательный путь — остановить diff и удалить лишний hunk либо вынести его в отдельный запрос. Не нужно объяснять расширение scope красивым комментарием. Сначала возвращают границу, затем отдельно обсуждают новую задачу и её владельца.

\n

Contract mismatch

\n

Контракт различает три входа: непустой ключ, пустую строку и недопустимый маркер. Первый нужно нормализовать. Второй означает отсутствие значения. Третий возвращает ошибку. Сгенерированный код может свести последние два случая к пустой строке. Такой код короче и проходит happy path, но теряет смысл ошибки.

\n

Проверка должна сравнивать не только типы и снимки ответа. Для каждого входа нужно назвать ожидаемый результат и запрещённый побочный эффект. Если invalid input не должен писать в базу, это условие обязано появиться в тесте. Иначе тест докажет лишь то, что функция что-то вернула.

\n

Test-evidence mismatch

\n

Тест может быть зелёным и не относиться к изменённой ветке. Например, он проверяет уникальный ключ, а diff добавляет запись до того, как обработает duplicate key. На duplicate-ветке результат должен быть ошибкой, а write helper не должен вызываться. Наличие файла с тестом не доказывает эту связь.

\n

Для каждой changed branch запишите три значения: input, expected output и forbidden side effect. Если ветка не имеет отдельного наблюдаемого условия, её следует считать непроверенной. Это правило действует и для кода, написанного человеком.

\n

Симптом → причина → проверка → действие

\n
Типовые ошибки перед merge
СимптомПричинаПроверкаДействие
В diff появился файл, которого нет в задачеПомощник расширил scope по соседнему контекстуСопоставить каждый changed path с карточкой задачиОстановить merge и вынести лишний hunk в отдельную задачу
Happy path зелёный, invalid input принятКод изменил смысл ошибки или absenceПрогнать фиксированные valid, absent и invalid входыВернуть различие в контракт и добавить negative test
Тест есть, но побочный эффект не проверенТест видит output, но не вызовы write helperПроверить число вызовов и порядок до errorЗафиксировать forbidden side effect и повторить тест
Линтер прошёл, поведение неизвестноПравило стиля не видит доменный контрактСверить ветки, статус, данные и права отдельноНе считать lint verdict доказательством correctness
Исправление выглядит локальным, но меняет доступСоседний security-sensitive код попал в контекстПоказать владельцу access path и отрицательные случаиОстановить merge до отдельного security review
\n

Иллюстрация границы

\n
\"Схема
Gate проверяет scope, контракт и evidence. Красная ветка означает stop до merge. Схема учебная: она не запускает CI, не выполняет rollback и не доказывает свойства production-системы.
\n

Схема полезна как напоминание о порядке. Сначала фиксируют границу задачи. Затем читают diff. После этого проверяют контракт и тест. Решение человека появляется в конце. Если начать с впечатления от кода, предыдущие вопросы легко пропустить.

\n

Порядок проверки перед merge

\n
  1. Сформулируйте задачу. Запишите цель, разрешённые файлы, запреты, владельца решения и ожидаемые evidence. Уберите секреты и персональные данные из контекста помощника.
  2. Прочитайте весь diff. Проверьте каждый path, импорт, условие, изменение схемы и вызов внешнего сервиса. Не ограничивайтесь hunk, который объясняет исходный симптом.
  3. Сверьте контракт. Назовите valid, absent и invalid входы. Для каждого укажите результат, статус или исключение и запрещённые побочные эффекты.
  4. Привяжите тест к ветке. Найдите тест, который действительно выполняет изменённое условие. Проверьте output, вызовы helper, права и состояние после ошибки.
  5. Прогоните отрицательный путь. Передайте неизвестное значение, пропущенное поле, неверный тип, duplicate или отказ в доступе — в зависимости от контракта. Ожидайте точный отказ, а не только отсутствие падения.
  6. Проверьте неизвестные. Отдельно запишите, чего не видно: всех ли потребителей нашли, совпадает ли runtime-конфигурация, проверены ли миграции, права и конкурентные вызовы. Unknown не равен pass.
  7. Примите ограниченное решение. Если всё доказано, человек одобряет конкретный diff. Если нет, reviewer запрашивает изменения, сужает scope или открывает отдельный риск. Не расширяйте разрешение молча.
\n

Ограничения и отрицательный путь

\n

Ни один слой не гарантирует correctness для всей системы. Модель может не знать скрытый consumer. Unit test может не увидеть реальный сериализатор. Линтер не знает, что 403 нельзя превращать в повторяемый 400. CI подтверждает только запущенные сценарии и окружение, в котором они запустились.

\n

Учебный код выше не заменяет review, contract test, security analysis, интеграционный запуск и процедуру отката. Он также не измеряет качество модели и не подтверждает экономию времени. В статье нет production-метрик и результатов реального внедрения. Примеры нужны только для проверки границы: что изменилось, какой вход это наблюдает и какой эффект запрещён.

\n

Если помощник удалил тест, ослабил проверку прав, изменил миграцию или добавил неизвестную зависимость, остановка важнее скорости. Верните diff к минимальному scope. Сохраните причину отказа. Повторно запросите только тот фрагмент, который можно проверить отдельным контрактом.

\n

Проверяемый критерий готовности

\n

Diff готов к решению о merge, когда reviewer может показать: каждый изменённый path разрешён задачей; для каждой изменённой ветки есть вход, ожидаемый результат и проверка запрещённого эффекта; отрицательные случаи возвращают согласованный отказ; lint и тесты выполнены в заявленном окружении; неизвестные записаны отдельно и не выданы за pass. Для изменения доступа, схемы или внешнего контракта нужен отдельный владелец соответствующего риска.

\n

Проверяемый результат — не фраза «код выглядит правильно». Это короткий набор ссылок на diff, contract rows и focused tests. Если хотя бы одна changed branch не связана с evidence, merge не готов. Такой критерий одинаково применим к ручному и AI-assisted коду.

\n

Проверяемые источники

" }