{ "index": 107, "slug": "editorial-2025-01-mechanism-ai-coding-assistant", "title": "Как принять код, предложенный AI-помощником", "excerpt": "AI-помощник ускоряет черновик, но не принимает решение за инженера. Разбираем, как проверить область изменения, контракт, отрицательный путь и зависимости до merge.", "contentHtml": "

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

\n

Ответ модели нужно считать кандидатом на изменение, а не доказательством корректности. Инженер проверяет четыре независимые границы: что разрешено менять, какое поведение описывает контракт, что происходит на отрицательном пути и какие данные ещё неизвестны. Если одна граница не подтверждена, естественный вид кода не делает его готовым к слиянию.

\n

Что именно делает помощник

\n

AI-кодинг-помощник продолжает код или предлагает фрагмент по доступному ему контексту: текущему файлу, открытым файлам, запросу и настройкам продукта. Такой контекст может быть полезным, но он не равен архитектуре репозитория. Скрытый consumer, правило авторизации, особый смысл пустого поля или ограничение версии API могут остаться за пределами запроса.

\n

Официальная документация GitHub описывает Copilot как инструмент, который помогает писать код и тесты, но не заменяет экспертизу пользователя. Для другого помощника нельзя автоматически переносить детали о контексте, фильтрах или хранении данных: их нужно сверять с документацией поставщика и политикой своей организации.

\n
Какой вопрос закрывает каждый слой проверки
СлойВопросНаблюдаемое свидетельствоЧего он не доказывает
Область измененияКакие пути и строки разрешены?Список файлов и hunks совпадает с задачейЧто новое поведение соответствует бизнес-правилу
КонтрактЧто разрешено для каждого класса входа?Таблица input → output и запрет побочного эффектаЧто реализация соблюдает контракт
ТестЧто реально произошло на выбранном входе?Тест проверяет changed branch и состояние после ошибкиЧто проверены все consumers, версии и нагрузки
ЧеловекКто принимает остаточный риск?Reviewer и owner видят diff, ограничения и результат проверокЧто неизвестных границ не существует
ИнструментКакие автоматические свойства проверены?Сборка, lint, security- и dependency-checksЧто инструмент понял доменный смысл
\n

Контракт нужно записать до diff

\n

До первого запроса сформулируйте не «сделай функцию», а маленькую карточку решения. В ней должны быть результат, разрешённые пути, запрещённые изменения, классы входов и способ проверки. Это не гарантирует хороший ответ. Зато объяснение модели не сможет незаметно заменить отсутствующее требование правдоподобной догадкой.

\n

Контракт должен различать хотя бы нормальный, пустой и невалидный вход. Для операции записи добавьте повторную операцию и запрет записи при ошибке. Если неизвестно, означает ли пустая строка «нет значения» или «ошибка», работу нельзя продолжать как будто это одно состояние: сначала нужен владелец контракта.

\n
const reviewCard = {\n  result: 'normalize one invoice key',\n  allowedPaths: ['src/parseInvoiceKey.js', 'test/parseInvoiceKey.test.js'],\n  forbidden: ['change authorization', 'add a default', 'add a dependency'],\n  cases: [\n    { input: 'invoice-42', output: 'invoice-42' },\n    { input: '', output: 'absent' },\n    { input: '?', output: 'invalid', writes: 0 }\n  ]\n};\n\n// Любое изменение вне allowedPaths требует нового решения.\n// Значения cases — часть учебного контракта, а не правило вашего API.
\n

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

\n

Как прочитать candidate diff

\n

Сначала смотрите не на объяснение помощника, а на область изменения. Лишний файл, новый пакет, изменение прав доступа, удалённый тест или новый default — повод остановиться. Малый размер diff не является доказательством низкого риска: одна строка в фильтре может изменить поведение всех пользователей.

\n

Затем прочитайте каждую изменённую ветку как условие: какой вход в неё попадает, какой результат выходит и какой побочный эффект запрещён. Отдельно проверьте код до первого write-вызова. Ошибка, возвращённая после записи, не эквивалентна ошибке без записи.

\n
git diff --name-only --diff-filter=ACMRT HEAD^ HEAD\ngit diff --check HEAD^ HEAD\ngit diff -- src/parseInvoiceKey.js test/parseInvoiceKey.test.js\nrg -n \"parseInvoiceKey|writeInvoice|authorization\" src test\nnpm test -- --runInBand
\n

Команды предполагают Git и npm-скрипт test; в проекте с pnpm, другой тестовой оболочкой или иной базовой ревизией замените только команду запуска. git diff --check ловит пробелы и конфликтные маркеры, но не проверяет семантику. rg помогает найти consumers, однако поиск по тексту не заменяет анализ динамического вызова или конфигурации.

\n
Матрица проверки AI-предложенного diff: область изменения, контракт, тест, человеческое решение и неизвестные границы.
Пять независимых вопросов перед merge. Схема показывает порядок рассуждения, а не измерение качества конкретной модели и не описание production-пайплайна.
\n

Отрицательный путь важнее красивого happy path

\n

Рассмотрим учебный parser, который различает ключ, отсутствие значения и недопустимый маркер. На нормальном входе invoice-42 возвращается тот же ключ. Пустая строка означает отсутствие значения. Символ ? означает ошибку. Помощник может предложить вернуть пустую строку для любого нераспознанного значения: позитивный тест останется зелёным, но два разных состояния сольются.

\n
function parseInvoiceKey(input) {\n  if (input === '') return { kind: 'absent' };\n  if (!/^invoice-[0-9]+$/.test(input)) return { kind: 'invalid' };\n  return { kind: 'value', value: input };\n}\n\nconst cases = [\n  ['invoice-42', { kind: 'value', value: 'invoice-42' }],\n  ['', { kind: 'absent' }],\n  ['?', { kind: 'invalid' }]\n];\n\nfor (const [input, expected] of cases) {\n  console.log(input, JSON.stringify(parseInvoiceKey(input)) === JSON.stringify(expected));\n}
\n

Сохраните фрагмент в файл review-example.mjs и запустите node review-example.mjs: он должен вывести три строки с true. Такой результат подтверждает только локальную функцию. Он не доказывает, что вызывающий код не пишет в базу при kind: 'invalid', что регулярное выражение подходит вашему формату или что параллельные запросы безопасны.

\n

Поэтому для реального кода тестируйте не только значение. Заставьте mock write-helper считать вызовы и проверьте ноль вызовов для недопустимого входа. Для повторной операции проверьте идемпотентность и состояние после второго запроса. Для authorization проверьте deny-ветку и отсутствие разрешения по умолчанию. Эти проверки должны быть привязаны к конкретному изменённому пути, иначе зелёный тест может относиться к старой реализации.

\n

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

\n
Диагностика правдоподобных ошибок в предложенном коде
СимптомВероятная причинаПроверкаДействие
Появился файл вне задачиКонтекст расширился по соседнему кодуСверить каждый path с allowedPaths и владельцемУдалить hunk или оформить отдельное решение
Happy path зелёный, invalid input не описанDefault подменил контрактДобавить таблицу классов входа и негативный тестНе принимать diff до решения владельца
Ошибка возвращается после write-вызоваПроверили output, но не side effectПроверить число и аргументы write-helperВалидировать до записи и покрыть тестом
Добавлен пакет с незнакомым именемМодель предложила несуществующую или неподходящую зависимостьПроверить registry, репозиторий, версию, лицензию и lockfileНе устанавливать до независимой проверки
Удалён падающий тестСимптом скрыли вместо исправления причиныСравнить diff тестов и причину исходного паденияВернуть тест или зафиксировать изменение требования
Линтер и сборка зелёные, результат неверенИнструменты не знают доменный смыслСопоставить branch с contract row и consumerДобавить поведенческий тест и review owner
\n

Проверка зависимости и контекста

\n

Новая зависимость заслуживает отдельной проверки. Убедитесь, что пакет существует в нужном реестре, поддерживает используемую версию runtime, имеет приемлемую лицензию и действительно нужен. Проверьте lockfile после установки и просмотрев diff убедитесь, что транзитивные пакеты не расширили риск неожиданно. Название, которое модель уверенно упомянула, не является фактом.

\n

Точно так же нельзя принимать объяснение «это стандартный API». Откройте документацию именно той версии библиотеки, которой пользуется проект, и найдите сигнатуру, ограничения и пример ошибки. Если помощник сослался на URL, проверьте его отдельно: ссылка должна вести на официальную документацию, а не подтверждать автоматически утверждение из ответа.

\n

Перед отправкой контекста удалите секреты, токены, персональные данные и ненужные фрагменты истории. Конкретные правила хранения и использования запросов зависят от поставщика, тарифного плана и настроек организации. Их нельзя выводить из поведения интерфейса. Для финансовых, медицинских, юридических и security-critical изменений заранее согласуйте допустимый инструмент и обязательный human review.

\n

Порядок действий до merge

\n
  1. Опишите симптом и цену ошибки: какой вход, endpoint или пользователь затронут и какое наблюдаемое поведение сейчас неверно.
  2. Запишите контракт до генерации: нормальный, пустой, невалидный и повторный входы, ожидаемый результат и запрещённый side effect.
  3. Назовите allowed paths, владельца контракта и список запретов: authorization, public format, dependencies, миграции или другие чувствительные границы.
  4. Просмотрите список файлов и hunks командой git diff до чтения объяснения модели. Любое расширение scope остановите.
  5. Сопоставьте каждую изменённую ветку с одной строкой контракта и найдите всех известных consumers через поиск и типы.
  6. Проверьте отрицательный путь: ошибку, число write-вызовов, состояние после отказа, повтор и права доступа, если они участвуют.
  7. Запустите focused test, сборку, lint и доступные security/dependency checks. Отдельно выполните git diff --check.
  8. Попросите reviewer проверить смысл и остаточный риск. Ответ помощника может быть входом в review, но не его результатом.
  9. Зафиксируйте неизвестное: скрытые consumers, нагрузка, совместимость версий, политика данных и то, что не запускалось в текущем окружении.
\n

Ограничения применимости

\n

Метод снижает риск, но не даёт гарантии. Небольшой тестовый набор не покрывает все комбинации, статический анализ не моделирует каждый runtime-путь, а reviewer может не знать скрытого потребителя. Даже официальные рекомендации конкретного поставщика описывают практику использования его продукта, а не корректность вашего доменного контракта.

\n

Для критичного изменения нужны дополнительные меры: владелец предметной области, security review, интеграционный тест, проверка миграции и план отката. Если нельзя проверить права, версию API или происхождение зависимости, правильный результат проверки — остановка и явно названный пробел. Не следует компенсировать отсутствие данных более уверенным prompt.

\n

Учебный parser и команды выше не являются готовым production-рецептом. Они не обращаются к базе, сети, CI или модели и не дают данных о скорости разработки. Их назначение уже: показать, как отделить input, output и forbidden side effect, затем связать их с изменённым кодом. В своём проекте замените значения примера на реальные правила и сохраните их рядом с тестом.

\n

Критерий готовности решения

\n

Решение можно обсуждать на merge, когда reviewer видит пять связей: каждый changed path разрешён задачей; каждая ветка связана с контрактом; отрицательный путь наблюдает и output, и отсутствие запрещённого side effect; зависимости и контекст проверены независимо; владелец принял остаточный риск. Это критерий достаточности свидетельств, а не обещание безошибочности.

\n

Если одна связь не видна, действие должно быть конкретным: сузить diff, добавить тест, проверить пакет, привлечь владельца или остановить изменение. Такая дисциплина сохраняет скорость черновика и не передаёт помощнику ответственность за контракт, которую может принять только команда.

\n

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

\n" }