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

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

\n

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

\n

Откуда берётся правдоподобная ошибка

\n

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

\n

Так возникает подмена ответственности. Модель выбирает поведение, которое кажется локально разумным. Инженер принимает его за восстановленное требование. Тест подтверждает только тот сценарий, который в него положили. Три разных утверждения сливаются в одно слово «проверено».

\n
Что именно доказывает каждый слой проверки
СлойВопросЧто можно подтвердитьЧего это не доказывает
Ответ моделиКакой код предложен?Текст diff и его локальная гипотезаЧто поведение разрешено контрактом
КонтрактЧто разрешено и запрещено?Вход, результат и forbidden side effectЧто diff соблюдает правило
ТестЧто наблюдалось на конкретной ветке?Связь input, output и побочного эффектаЧто покрыты все потребители и среды
РевьюПочему принят этот scope?Пути файлов, владельца и решение о границеЧто runtime ведёт себя так же
НеизвестноеКаких данных нет?Честно названную непроверенную границуОтсутствие риска
\n

Учебный пример: форматирование заметки

\n

Ниже — изолированный учебный пример. Он не обращается к модели, базе данных, CI или production-сервису. В контракте есть три различающихся входа. Пустая строка означает отсутствие заметки. Невалидный маркер означает ошибку входа. Эти результаты нельзя объединять без решения владельца интерфейса.

\n
const cases = [\n  { input: 'note', expected: 'formatted-note' },\n  { input: 'blank', expected: 'absent-note' },\n  { input: 'invalid-marker', expected: 'invalid-note' },\n];\n\nfunction formatNote(input) {\n  if (input === 'blank') return 'absent-note';\n  if (input === 'invalid-marker') return 'invalid-note';\n  return `formatted-${input}`;\n}\n\n// Учебный контракт: invalid-marker нельзя превращать в ''.
\n

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

\n
expect(formatNote('note')).toBe('formatted-note');\nexpect(formatNote('blank')).toBe('absent-note');\nexpect(formatNote('invalid-marker')).toBe('invalid-note');\n\n// Отдельное требование для вызывающего кода:\n// invalid-marker не должен вызывать writeNote().
\n

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

\n
\"Матрица
Схема помогает разделить четыре вопроса до merge. Это учебная модель проверки, а не классификация production-инцидентов и не измерение качества какой-либо модели.
\n

Как читать diff по границам

\n

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

\n

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

\n

После этого свяжите тест с изменённой веткой. Тест должен называть вход, ожидаемый результат и запрещённое действие. Проверка соседней ветки не покрывает новую ветку. Линтер подтверждает форму кода. Типы подтверждают часть интерфейса. Ни один из них не восстанавливает доменное правило, которое нигде не записано.

\n

Симптомы и точечные проверки

\n
Диагностическая таблица для candidate diff
СимптомПричинаПроверкаДействие
Изменён файл, которого нет в задачеScope расширился по соседнему контекстуСверить каждый path с формулировкой и владельцемОстановить diff или оформить отдельное решение
Happy path зелёный, invalid input не описанМодель выбрала default вместо контрактаДобавить таблицу классов входа и негативный тестВернуть код к владельцу контракта
Ошибка возвращается после write-вызоваРезультат проверили, side effect — нетПроверить число и аргументы write-вызововЗапретить запись до валидации
Есть тест, но он не касается changed branchТест подтверждает другую веткуСвязать branch с конкретным input и expected outputДобавить focused negative case
Ревьюер говорит «выглядит безопасно»Неизвестная граница принята за отсутствие рискаСоставить список непроверенных consumers, прав и версийСузить обещание или получить недостающее evidence
\n

Порядок действий перед принятием

\n
  1. Сформулируйте задачу в одном абзаце: какие пути можно менять и какой результат нужен.
  2. Отделите ответ помощника от решения. Сохраните candidate diff, но не называйте его исправлением.
  3. Выпишите контракт для каждой изменённой ветки: вход, разрешённый результат и запрещённый side effect.
  4. Проверьте scope по списку файлов и строк. Каждый выход за границу требует отдельного владельца и решения.
  5. Запустите focused tests для happy path, пустого значения, невалидного значения и повторной операции, если она возможна.
  6. Проверьте отрицательный путь по наблюдаемому следу: результат ошибки, количество вызовов и состояние после отказа.
  7. Отдельно перечислите неизвестное: реальные потребители, совместимость версий, права, конкурентный доступ и нагрузка.
  8. Примите diff только после того, как reviewer может показать конкретное evidence для каждой изменённой границы.
\n

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

\n

Такая проверка снижает риск, но не превращает код в гарантированно корректный. Focused test может пропустить редкую последовательность. Ревью может не знать о скрытом потребителе. Статический анализ не моделирует все права и состояния. Само наличие источника или пояснения модели не заменяет запусков и проверки доменного контракта.

\n

Учебный пример выше намеренно мал. Он не даёт данных о конкретном помощнике, модели, репозитории, скорости разработки или production-ошибках. Для чувствительного кода нужно дополнительно ограничить доступ к контексту, проверить секреты, просмотреть зависимости и согласовать правила хранения исходников. Если нет данных о совместимости или владельце результата, корректное действие — остановиться и назвать пробел, а не заполнить его догадкой.

\n

Проверяемый критерий готовности можно сформулировать жёстко: для каждого изменённого пути есть владелец и разрешённый scope; для каждой изменённой ветки есть contract row; для отрицательного пути зафиксированы output и forbidden side effect; тест наблюдает именно эту ветку; неизвестные перечислены отдельно. Если один пункт отсутствует, готовность не доказана. Это не означает, что изменение нельзя сделать. Это означает, что решение требует ещё одного факта.

\n

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

\n" }