Files
progcode/editorial/agent-rewrites/108.json
T

8 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 &lt;&lt;'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>"
}