{ "index": 107, "slug": "editorial-2025-01-mechanism-ai-coding-assistant", "title": "Как проверить код, предложенный AI-помощником", "excerpt": "AI-помощник быстро создаёт правдоподобный diff, но не знает контракт конкретного репозитория. Разбираем, как найти нарушение границы, проверить отрицательный путь и принять решение по наблюдаемым данным.", "contentHtml": "
Самая дорогая ошибка AI-помощника часто выглядит как хороший результат. Diff компилируется. Имена понятны. Форматирование проходит. Тест happy path возвращает ожидаемую строку. После merge выясняется, что невалидный маркер превратился в пустое значение или повторный ключ успел вызвать запись перед возвратом ошибки. Команда платит не только за исправление. Она восстанавливает прежний контракт, ищет затронутых потребителей и разбирает, почему зелёная проверка пропустила проблему.
\nТезис простой: ответ модели — кандидат на изменение, а не доказательство корректности. Чтобы принять такой diff, нужно раздельно проверить область изменения, контракт, отрицательный путь и неизвестные границы. Если хотя бы одна граница не подтверждена, код нельзя считать готовым только потому, что он выглядит естественно.
\nПомощник продолжает текст по контексту задачи. Он видит имена функций, соседний код и формулировку запроса. Но репозиторий хранит больше правил, чем попало в контекст: допустимые значения, права, порядок побочных эффектов, требования потребителей, версию внешнего API и смысл пустого результата. Когда правило не выражено явно, кандидат заполняет пробел самым удобным вариантом.
\nТак возникает подмена ответственности. Модель выбирает поведение, которое кажется локально разумным. Инженер принимает его за восстановленное требование. Тест подтверждает только тот сценарий, который в него положили. Три разных утверждения сливаются в одно слово «проверено».
\n| Слой | Вопрос | Что можно подтвердить | Чего это не доказывает |
|---|---|---|---|
| Ответ модели | Какой код предложен? | Текст diff и его локальная гипотеза | Что поведение разрешено контрактом |
| Контракт | Что разрешено и запрещено? | Вход, результат и forbidden side effect | Что diff соблюдает правило |
| Тест | Что наблюдалось на конкретной ветке? | Связь input, output и побочного эффекта | Что покрыты все потребители и среды |
| Ревью | Почему принят этот scope? | Пути файлов, владельца и решение о границе | Что runtime ведёт себя так же |
| Неизвестное | Каких данных нет? | Честно названную непроверенную границу | Отсутствие риска |
Ниже — изолированный учебный пример. Он не обращается к модели, базе данных, CI или production-сервису. В контракте есть три различающихся входа. Пустая строка означает отсутствие заметки. Невалидный маркер означает ошибку входа. Эти результаты нельзя объединять без решения владельца интерфейса.
\nconst 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. Если функция вызывается перед записью, нужно проверить ещё и запрет записи при ошибочном входе.
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Сначала проверьте scope. Сравните каждый изменённый путь с задачей. Соседний полезный hunk не становится разрешённым автоматически. Если помощник добавил обработчик, конфигурацию или вызов в другом модуле, остановите проверку и получите отдельное решение владельца. Иначе локальная оптимизация расширит поверхность изменения незаметно.
\nЗатем зафиксируйте контракт до обсуждения стиля. Запишите допустимые входы, результат для каждого класса входов и побочный эффект, которого быть не должно. Важны не только возвращаемые значения. Для операции создания записи дубликат может вернуть ошибку и не сделать ни одного write-вызова. Если такой запрет не назван, зелёный тест на ошибку не доказывает безопасность ветки.
\nПосле этого свяжите тест с изменённой веткой. Тест должен называть вход, ожидаемый результат и запрещённое действие. Проверка соседней ветки не покрывает новую ветку. Линтер подтверждает форму кода. Типы подтверждают часть интерфейса. Ни один из них не восстанавливает доменное правило, которое нигде не записано.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Изменён файл, которого нет в задаче | Scope расширился по соседнему контексту | Сверить каждый path с формулировкой и владельцем | Остановить diff или оформить отдельное решение |
| Happy path зелёный, invalid input не описан | Модель выбрала default вместо контракта | Добавить таблицу классов входа и негативный тест | Вернуть код к владельцу контракта |
| Ошибка возвращается после write-вызова | Результат проверили, side effect — нет | Проверить число и аргументы write-вызовов | Запретить запись до валидации |
| Есть тест, но он не касается changed branch | Тест подтверждает другую ветку | Связать branch с конкретным input и expected output | Добавить focused negative case |
| Ревьюер говорит «выглядит безопасно» | Неизвестная граница принята за отсутствие риска | Составить список непроверенных consumers, прав и версий | Сузить обещание или получить недостающее evidence |
Такая проверка снижает риск, но не превращает код в гарантированно корректный. Focused test может пропустить редкую последовательность. Ревью может не знать о скрытом потребителе. Статический анализ не моделирует все права и состояния. Само наличие источника или пояснения модели не заменяет запусков и проверки доменного контракта.
\nУчебный пример выше намеренно мал. Он не даёт данных о конкретном помощнике, модели, репозитории, скорости разработки или production-ошибках. Для чувствительного кода нужно дополнительно ограничить доступ к контексту, проверить секреты, просмотреть зависимости и согласовать правила хранения исходников. Если нет данных о совместимости или владельце результата, корректное действие — остановиться и назвать пробел, а не заполнить его догадкой.
\nПроверяемый критерий готовности можно сформулировать жёстко: для каждого изменённого пути есть владелец и разрешённый scope; для каждой изменённой ветки есть contract row; для отрицательного пути зафиксированы output и forbidden side effect; тест наблюдает именно эту ветку; неизвестные перечислены отдельно. Если один пункт отсутствует, готовность не доказана. Это не означает, что изменение нельзя сделать. Это означает, что решение требует ещё одного факта.
\n