{ "index": 108, "slug": "editorial-2025-01-practice-ai-coding-assistant", "title": "AI-помощник в разработке: как проверить и принять ограниченный diff", "excerpt": "AI-помощник ускоряет черновик, но не знает скрытый контракт репозитория. Показываю, как ограничить контекст, проверить отрицательные ветки и принять только diff с наблюдаемым evidence.", "contentHtml": "
Разработчик просит AI-помощника исправить обработку ключа. В ответ приходит аккуратный diff: имена совпадают со стилем проекта, happy path проходит, объяснение звучит уверенно. После merge выясняется, что невалидный маркер превратился в пустое значение, а запись в хранилище выполняется до проверки. Ошибка проявилась не в синтаксисе, а на границе контракта: программа вернула допустимое по типам, но неверное по смыслу значение. Цена такого промаха — повторное ревью, поиск скрытых потребителей и риск изменить права или данные без явного решения владельца.
\nБезопасная единица работы здесь — не ответ модели, а ограниченный candidate diff. Помощник может ускорить черновик и подсветить варианты, но инженер задаёт допустимый результат, запрещённые изменения и способ наблюдения. Reviewer принимает область и остаточный риск. Тест проверяет конкретные ветки. Если хотя бы одна из этих границ не названа, красивый ответ ещё не является исправлением.
\nУ предложения AI есть три разных свойства, которые часто ошибочно объединяют словом «готово». Оно может быть синтаксически корректным, соответствовать локальному стилю и всё же нарушать бизнес-правило. Поэтому сначала разделите вопросы: что предложено, где это изменяет систему, какое поведение разрешено и что наблюдалось на проверочном входе.
\nКонтекст тоже имеет границу. Помощник видит переданные файлы, открытые участки или доступные ему сведения, но не получает автоматически смысл каждого потребителя, права на запись, версию внешнего сервиса и последствия пустого значения. Отсутствующее правило не становится безопасным правилом. Если его нельзя подтвердить в коде, документации или у владельца, его следует записать как неизвестное.
\n| Слой | Вопрос | Наблюдаемое свидетельство | Чего оно не доказывает |
|---|---|---|---|
| Scope | Какие пути и строки разрешено менять? | Список changed paths и diff hunks | Что новое поведение соответствует домену |
| Контракт | Что должно произойти для каждого класса входа? | Таблица input → output → side effect | Что реализация действительно соблюдает таблицу |
| Тест | Что произошло на выбранной ветке? | Результат теста и наблюдение вызовов | Что проверены все потребители, среды и нагрузки |
| Владелец | Кто принимает смысл и остаточный риск? | Явное решение domain или security owner | Что runtime не отличается от тестовой среды |
До первого запроса запишите одну задачу и её отрицательные условия. «Исправь parser» слишком широко: помощник может изменить формат ошибки, добавить default, обновить зависимость и затронуть соседний обработчик. «Для parseInvoiceKey различай пустой ввод и невалидный маркер; меняй только реализацию и тест; не добавляй default и не трогай авторизацию» уже задаёт проверяемую границу.
const task = {\n goal: 'parse one invoice key',\n allowedPaths: ['src/invoice-key.js', 'test/invoice-key.test.js'],\n contract: [\n ['invoice-42', 'value:invoice-42', 'no write'],\n ['', 'absent', 'no write'],\n ['?', 'invalid', 'no write']\n ],\n forbidden: ['new default', 'authorization change', 'dependency change'],\n owner: 'invoice-contract-owner',\n evidence: ['path list', 'negative test', 'human review']\n};\n\n// Это карточка задачи, а не разрешение принять любой ответ модели.\n// Выход за allowedPaths останавливает ревью.\nКарточка нужна не для красивого prompt, а для сравнения с результатом. В ней должны быть допустимые пути, ожидаемый результат и запрещённый side effect. Секреты, персональные данные и лишнюю историю в контекст не передают. Для изменения авторизации, платежа, миграции схемы или внешнего API границу дополнительно подтверждают владельцем риска; модель не может назначить себе такие полномочия.
\nНа локальном входе «invoice-42» несколько реализаций выглядят одинаково. Различие появляется на границе: пустая строка — это отсутствие значения, а «?» — ошибка входа. Если функция возвращает '' для обоих случаев, happy path остаётся зелёным, но вызывающий код теряет возможность отличить «не передано» от «повреждено». Это не абстрактный риск генерации: это конкретная потеря состояния.
Ниже — полностью локальный пример. Он не обращается к модели, репозиторию, сети или реальным данным. Его задача — сделать отрицательную ветку видимой и показать, почему тест на одном положительном значении недостаточен.
\nnode --input-type=module <<'NODE'\nfunction parseInvoiceKey(input) {\n if (input === '') return { kind: 'absent' };\n if (!/^invoice-\\d+$/.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 const actual = parseInvoiceKey(input);\n if (JSON.stringify(actual) !== JSON.stringify(expected)) {\n throw new Error(`${input}: ${JSON.stringify(actual)}`);\n }\n}\nconsole.log('3 contract cases passed');\nNODE\nКоманда запускается в shell с установленным Node.js и должна вывести 3 contract cases passed. Версия Node.js, формат запуска и набор тестов — свойства конкретного проекта, поэтому перед копированием примера их сверяют с локальным toolchain. Если candidate заменит ветку invalid на absent, третья проверка упадёт. Если parser вызывается перед записью, одного результата недостаточно: вызывающий код должен доказать, что при invalid write-helper не вызывается.
Сначала просмотрите список файлов, а затем hunks. Не начинайте с объяснения модели: оно может описывать намерение, но не скрытые изменения. Соседний «полезный» файл не становится разрешённым автоматически. Новый импорт, изменение конфигурации, другой обработчик ошибки или удалённый тест — отдельный вопрос для владельца.
\n# Выполнить из корня git-репозитория после получения candidate diff\ngit diff --name-only\ngit diff --check\ngit diff -- src/invoice-key.js test/invoice-key.test.js\n\n# Затем запустить реальную команду тестов проекта, например:\nnpm test -- --runInBand\ngit diff --name-only показывает область изменения, а git diff --check находит пробелы и конфликтные маркеры, но ни одна из команд не проверяет доменный смысл. Последняя строка — только форма вызова: флаг --runInBand поддерживается не каждым test runner, поэтому её заменяют на команду, принятую в проекте. Нельзя объявлять тест зелёным, если команда не запускалась или запускала не тот набор файлов.
| Симптом | Гипотеза | Проверка | Решение |
|---|---|---|---|
| Изменён путь вне карточки | Контекст расширился сам | Сравнить каждый путь с allowedPaths | Убрать hunk или открыть отдельное решение |
| Happy path зелёный, invalid не описан | Неявный default заменил контракт | Добавить normal, blank, invalid и duplicate cases | Не принимать diff до решения владельца |
| Ошибка возвращается после write-вызова | Проверен output, но не side effect | Проверить число, аргументы и порядок вызовов | Валидировать до изменения состояния |
| Добавлена новая зависимость | Локальная задача превратилась в расширение supply chain | Проверить пакет, версию, лицензию и необходимость | Удалить или провести отдельное dependency review |
| Комментарий модели уверенный, evidence нет | Объяснение приняли за факт | Повторить проверку кодом, тестом и документацией | Оставить решение на hold |
Негативная проверка должна наблюдать два значения: что вернула функция и чего она не сделала. Для parser это kind: 'invalid' и ноль вызовов записи. Для авторизации — отказ и отсутствие allow по умолчанию. Для миграции — понятная ошибка и сохранение исходного состояния. Для внешнего API — корректная обработка timeout, 4xx и повторного запроса. Название теста должно связывать вход, ожидаемый результат и запрещённое действие.
Проверка зависимостей — отдельный слой. Генератор может предложить несуществующий пакет, неверную версию или код с несовместимой лицензией. Установка зависимости до проверки имени и источника расширяет поверхность атаки и усложняет откат. Поэтому сначала ищут уже используемый механизм в репозитории, затем сверяют официальную документацию пакета и только после этого меняют manifest и lockfile. Если dependency diff не входил в задачу, он остаётся за её границей.
\nЭтот маршрут уменьшает риск, но не делает генерацию источником истины. Ограниченный контекст не раскрывает скрытого потребителя. Unit-тест проверяет выбранные случаи, а не все комбинации. Линтер и типы подтверждают форму интерфейса, но не смысл бизнес-правила. Человеческое ревью тоже ошибается, особенно если владелец контракта не участвует.
\nК критическим участкам применяйте более строгий процесс. Для authentication, платежей, персональных данных, медицинских решений, миграций и необратимых операций нужны дополнительные владельцы, threat model, интеграционные проверки и понятный rollback. Не передавайте внешнему сервису секреты и фрагменты кода, если политика проекта этого не разрешает. Правила хранения, обучения и удаления данных зависят от конкретного инструмента и тарифа; их нельзя выводить из общего слова «AI».
\nОфициальные рекомендации GitHub сводят ревью AI-кода к функциональным проверкам, сверке контекста и намерения, проверке зависимостей, поиску выдуманных API и пропущенных ограничений, совместному ревью и автоматизации. Там же прямо сказано, что предложения нужно проверять и тестировать, особенно для критичных и чувствительных приложений. NIST SP 800-218A дополняет SSDF практиками для разработки систем с generative AI и предназначен для применения вместе с SSDF 1.1. Это рамки и направления проверки, а не готовый тест вашего репозитория.
\nCandidate diff можно выносить на решение о merge, когда для каждого изменённого пути виден scope, для каждой ветки есть контрактная строка, отказной путь проверяет output и side effect, а тесты действительно запускались на изменённой реализации. Владелец назван и принял остаточный риск. Неизвестные потребители, версии и среда перечислены отдельно. Если одного элемента нет, действие однозначно: сузить diff, добавить evidence, привлечь владельца или остановить merge.
\nПольза помощника — в скорости перебора вариантов, а не в передаче ему ответственности. Надёжное решение оставляет после себя читаемый diff, воспроизводимую проверку и понятную причину, по которой изменение разрешено. Такой результат можно проверить через неделю другим инженером и отличить от правдоподобной, но неверной догадки.
\n