8 lines
23 KiB
JSON
8 lines
23 KiB
JSON
{
|
||
"index": 108,
|
||
"slug": "editorial-2025-01-practice-ai-coding-assistant",
|
||
"title": "AI-помощник в разработке: как проверить и принять ограниченный diff",
|
||
"excerpt": "AI-помощник ускоряет черновик, но не знает скрытый контракт репозитория. Показываю, как ограничить контекст, проверить отрицательные ветки и принять только diff с наблюдаемым evidence.",
|
||
"contentHtml": "<p>Разработчик просит AI-помощника исправить обработку ключа. В ответ приходит аккуратный diff: имена совпадают со стилем проекта, happy path проходит, объяснение звучит уверенно. После merge выясняется, что невалидный маркер превратился в пустое значение, а запись в хранилище выполняется до проверки. Ошибка проявилась не в синтаксисе, а на границе контракта: программа вернула допустимое по типам, но неверное по смыслу значение. Цена такого промаха — повторное ревью, поиск скрытых потребителей и риск изменить права или данные без явного решения владельца.</p>\n<p>Безопасная единица работы здесь — не ответ модели, а ограниченный candidate diff. Помощник может ускорить черновик и подсветить варианты, но инженер задаёт допустимый результат, запрещённые изменения и способ наблюдения. Reviewer принимает область и остаточный риск. Тест проверяет конкретные ветки. Если хотя бы одна из этих границ не названа, красивый ответ ещё не является исправлением.</p>\n<h2>Что именно проверяет команда</h2>\n<p>У предложения AI есть три разных свойства, которые часто ошибочно объединяют словом «готово». Оно может быть синтаксически корректным, соответствовать локальному стилю и всё же нарушать бизнес-правило. Поэтому сначала разделите вопросы: что предложено, где это изменяет систему, какое поведение разрешено и что наблюдалось на проверочном входе.</p>\n<p>Контекст тоже имеет границу. Помощник видит переданные файлы, открытые участки или доступные ему сведения, но не получает автоматически смысл каждого потребителя, права на запись, версию внешнего сервиса и последствия пустого значения. Отсутствующее правило не становится безопасным правилом. Если его нельзя подтвердить в коде, документации или у владельца, его следует записать как неизвестное.</p>\n<table><caption>Четыре вопроса перед принятием AI-предложения</caption><thead><tr><th scope='col'>Слой</th><th scope='col'>Вопрос</th><th scope='col'>Наблюдаемое свидетельство</th><th scope='col'>Чего оно не доказывает</th></tr></thead><tbody><tr><td>Scope</td><td>Какие пути и строки разрешено менять?</td><td>Список changed paths и diff hunks</td><td>Что новое поведение соответствует домену</td></tr><tr><td>Контракт</td><td>Что должно произойти для каждого класса входа?</td><td>Таблица input → output → side effect</td><td>Что реализация действительно соблюдает таблицу</td></tr><tr><td>Тест</td><td>Что произошло на выбранной ветке?</td><td>Результат теста и наблюдение вызовов</td><td>Что проверены все потребители, среды и нагрузки</td></tr><tr><td>Владелец</td><td>Кто принимает смысл и остаточный риск?</td><td>Явное решение domain или security owner</td><td>Что runtime не отличается от тестовой среды</td></tr></tbody></table>\n<h2>Сначала зафиксируйте границу</h2>\n<p>До первого запроса запишите одну задачу и её отрицательные условия. «Исправь parser» слишком широко: помощник может изменить формат ошибки, добавить default, обновить зависимость и затронуть соседний обработчик. «Для <code>parseInvoiceKey</code> различай пустой ввод и невалидный маркер; меняй только реализацию и тест; не добавляй default и не трогай авторизацию» уже задаёт проверяемую границу.</p>\n<pre><code>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 останавливает ревью.</code></pre>\n<p>Карточка нужна не для красивого prompt, а для сравнения с результатом. В ней должны быть допустимые пути, ожидаемый результат и запрещённый side effect. Секреты, персональные данные и лишнюю историю в контекст не передают. Для изменения авторизации, платежа, миграции схемы или внешнего API границу дополнительно подтверждают владельцем риска; модель не может назначить себе такие полномочия.</p>\n<h2>Почему правдоподобный diff ошибается</h2>\n<p>На локальном входе «invoice-42» несколько реализаций выглядят одинаково. Различие появляется на границе: пустая строка — это отсутствие значения, а «?» — ошибка входа. Если функция возвращает <code>''</code> для обоих случаев, happy path остаётся зелёным, но вызывающий код теряет возможность отличить «не передано» от «повреждено». Это не абстрактный риск генерации: это конкретная потеря состояния.</p>\n<figure><img src='/assets/editorial/2025/ai-coding-assistant-2025-prompt-diff-review.svg' alt='Схема проверки AI-предложения: карточка задачи задаёт контекст и запреты, candidate diff проходит сверку scope, review и тест; выход за границу ведёт к остановке до merge.' loading='lazy' /><figcaption>Рисунок 1. Candidate diff проходит четыре независимые проверки. Схема объясняет порядок рассуждения и не является отчётом о работе конкретной модели или production-пайплайна.</figcaption></figure>\n<p>Ниже — полностью локальный пример. Он не обращается к модели, репозиторию, сети или реальным данным. Его задача — сделать отрицательную ветку видимой и показать, почему тест на одном положительном значении недостаточен.</p>\n<pre><code>node --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</code></pre>\n<p>Команда запускается в shell с установленным Node.js и должна вывести <code>3 contract cases passed</code>. Версия Node.js, формат запуска и набор тестов — свойства конкретного проекта, поэтому перед копированием примера их сверяют с локальным toolchain. Если candidate заменит ветку <code>invalid</code> на <code>absent</code>, третья проверка упадёт. Если parser вызывается перед записью, одного результата недостаточно: вызывающий код должен доказать, что при <code>invalid</code> write-helper не вызывается.</p>\n<h2>Читайте diff от границы к смыслу</h2>\n<p>Сначала просмотрите список файлов, а затем hunks. Не начинайте с объяснения модели: оно может описывать намерение, но не скрытые изменения. Соседний «полезный» файл не становится разрешённым автоматически. Новый импорт, изменение конфигурации, другой обработчик ошибки или удалённый тест — отдельный вопрос для владельца.</p>\n<pre><code># Выполнить из корня 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</code></pre>\n<p><code>git diff --name-only</code> показывает область изменения, а <code>git diff --check</code> находит пробелы и конфликтные маркеры, но ни одна из команд не проверяет доменный смысл. Последняя строка — только форма вызова: флаг <code>--runInBand</code> поддерживается не каждым test runner, поэтому её заменяют на команду, принятую в проекте. Нельзя объявлять тест зелёным, если команда не запускалась или запускала не тот набор файлов.</p>\n<table><caption>Симптом candidate diff и следующий шаг</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Гипотеза</th><th scope='col'>Проверка</th><th scope='col'>Решение</th></tr></thead><tbody><tr><td>Изменён путь вне карточки</td><td>Контекст расширился сам</td><td>Сравнить каждый путь с allowedPaths</td><td>Убрать hunk или открыть отдельное решение</td></tr><tr><td>Happy path зелёный, invalid не описан</td><td>Неявный default заменил контракт</td><td>Добавить normal, blank, invalid и duplicate cases</td><td>Не принимать diff до решения владельца</td></tr><tr><td>Ошибка возвращается после write-вызова</td><td>Проверен output, но не side effect</td><td>Проверить число, аргументы и порядок вызовов</td><td>Валидировать до изменения состояния</td></tr><tr><td>Добавлена новая зависимость</td><td>Локальная задача превратилась в расширение supply chain</td><td>Проверить пакет, версию, лицензию и необходимость</td><td>Удалить или провести отдельное dependency review</td></tr><tr><td>Комментарий модели уверенный, evidence нет</td><td>Объяснение приняли за факт</td><td>Повторить проверку кодом, тестом и документацией</td><td>Оставить решение на hold</td></tr></tbody></table>\n<h2>Проверяйте не только результат, но и отказ</h2>\n<p>Негативная проверка должна наблюдать два значения: что вернула функция и чего она не сделала. Для parser это <code>kind: 'invalid'</code> и ноль вызовов записи. Для авторизации — отказ и отсутствие allow по умолчанию. Для миграции — понятная ошибка и сохранение исходного состояния. Для внешнего API — корректная обработка timeout, 4xx и повторного запроса. Название теста должно связывать вход, ожидаемый результат и запрещённое действие.</p>\n<p>Проверка зависимостей — отдельный слой. Генератор может предложить несуществующий пакет, неверную версию или код с несовместимой лицензией. Установка зависимости до проверки имени и источника расширяет поверхность атаки и усложняет откат. Поэтому сначала ищут уже используемый механизм в репозитории, затем сверяют официальную документацию пакета и только после этого меняют manifest и lockfile. Если dependency diff не входил в задачу, он остаётся за её границей.</p>\n<h2>Минимальный маршрут до merge</h2>\n<ol><li><strong>Опишите один результат.</strong> Назовите вход, ожидаемый output и побочный эффект, которого быть не должно.</li><li><strong>Сузьте контекст.</strong> Передайте нужную сигнатуру, контракт, связанные тесты и правила проекта; исключите секреты и лишнюю историю.</li><li><strong>Зафиксируйте allowedPaths.</strong> Список файлов и запреты должны появиться до генерации. Любой новый путь остановите.</li><li><strong>Просмотрите diff.</strong> Проверьте файлы, импорты, defaults, удалённые тесты, зависимости, права и изменения публичного формата.</li><li><strong>Сверьте четыре класса входа.</strong> Проверьте normal, blank, invalid и duplicate или retry, если они возможны в вашем домене.</li><li><strong>Наблюдайте side effect.</strong> Убедитесь, что отказ не вызывает запись, отправку, выдачу права или повторную операцию.</li><li><strong>Запустите focused checks.</strong> Используйте реальные команды проекта и сохраните результат; затем добавьте интеграционные, security или performance checks по риску.</li><li><strong>Проведите человеческое ревью.</strong> Владелец контракта принимает смысл, reviewer — scope и читаемость, security или data owner подключается при чувствительном изменении.</li><li><strong>Запишите неизвестное.</strong> Отдельно перечислите непроверенных consumers, версии, нагрузку и различия сред. Не превращайте отсутствие данных в зелёный статус.</li></ol>\n<h2>Границы применения</h2>\n<p>Этот маршрут уменьшает риск, но не делает генерацию источником истины. Ограниченный контекст не раскрывает скрытого потребителя. Unit-тест проверяет выбранные случаи, а не все комбинации. Линтер и типы подтверждают форму интерфейса, но не смысл бизнес-правила. Человеческое ревью тоже ошибается, особенно если владелец контракта не участвует.</p>\n<p>К критическим участкам применяйте более строгий процесс. Для authentication, платежей, персональных данных, медицинских решений, миграций и необратимых операций нужны дополнительные владельцы, threat model, интеграционные проверки и понятный rollback. Не передавайте внешнему сервису секреты и фрагменты кода, если политика проекта этого не разрешает. Правила хранения, обучения и удаления данных зависят от конкретного инструмента и тарифа; их нельзя выводить из общего слова «AI».</p>\n<p>Официальные рекомендации GitHub сводят ревью AI-кода к функциональным проверкам, сверке контекста и намерения, проверке зависимостей, поиску выдуманных API и пропущенных ограничений, совместному ревью и автоматизации. Там же прямо сказано, что предложения нужно проверять и тестировать, особенно для критичных и чувствительных приложений. NIST SP 800-218A дополняет SSDF практиками для разработки систем с generative AI и предназначен для применения вместе с SSDF 1.1. Это рамки и направления проверки, а не готовый тест вашего репозитория.</p>\n<h2>Критерий готовности</h2>\n<p>Candidate diff можно выносить на решение о merge, когда для каждого изменённого пути виден scope, для каждой ветки есть контрактная строка, отказной путь проверяет output и side effect, а тесты действительно запускались на изменённой реализации. Владелец назван и принял остаточный риск. Неизвестные потребители, версии и среда перечислены отдельно. Если одного элемента нет, действие однозначно: сузить diff, добавить evidence, привлечь владельца или остановить merge.</p>\n<p>Польза помощника — в скорости перебора вариантов, а не в передаче ему ответственности. Надёжное решение оставляет после себя читаемый diff, воспроизводимую проверку и понятную причину, по которой изменение разрешено. Такой результат можно проверить через неделю другим инженером и отличить от правдоподобной, но неверной догадки.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://docs.github.com/en/copilot/tutorials/review-ai-generated-code' target='_blank' rel='noopener noreferrer'>GitHub Docs: Review AI-generated code</a> — официальное руководство по функциональным проверкам, контексту и намерению, зависимостям, AI-специфичным ошибкам, совместному ревью и автоматизации.</li><li><a href='https://docs.github.com/en/copilot/responsible-use/inline-suggestions' target='_blank' rel='noopener noreferrer'>GitHub Docs: Application card — GitHub Copilot inline suggestions</a> — описывает ограниченность контекста, риск неточного или небезопасного кода, необходимость человеческого контроля и проверки перед принятием.</li><li><a href='https://csrc.nist.gov/pubs/sp/800/218/a/final' target='_blank' rel='noopener noreferrer'>NIST SP 800-218A: Secure Software Development Practices for Generative AI and Dual-Use Foundation Models</a> — финальная публикация июля 2024 года; дополняет SSDF 1.1 практиками и задачами для AI-разработки и не заменяет локальный контракт, тесты или владельца риска.</li></ul>"
|
||
}
|