DarkRiDDeR12 мин

Почему правдоподобный код ломает контракт

AIКачество

Самая неприятная ошибка помощника не обязана выглядеть как ошибка. Diff компилируется, стиль ровный, название функции понятное, а happy path даёт ожидаемую строку. Затем выясняется, что invalid input превратился в новый default или duplicate branch успевает выполнить side effect. Цена решения здесь выше, чем один фикс: команда теряет прежний контракт и тратит review на восстановление того, что не было зафиксировано до генерации.

Причина в смешении пяти разных предметов. Model output — это предложенный текст. Repository contract — правило о входе, выходе и запретах. Reviewer evidence — объяснение, почему diff попадает в scope. Test evidence — наблюдение конкретной ветки. Unknowns — то, чего эти данные не показывают. Пока они лежат в одном слове «проверено», правдоподобный код получает полномочия, которых у него нет. В январе 2025 GitHub прямо предупреждал, что код может выглядеть валидным, но не соответствовать намерению разработчика; поэтому процесс должен делить доказательства, а не усиливать уверенность в одном ответе.

Пять слоёв, которые нельзя склеивать

Что именно известно после candidate diff
СлойНа какой вопрос отвечаетПример fixed synthetic evidenceЧего не доказывает
Model outputКакой текст был предложен?Ветка возвращает empty string для invalid marker.Что это допустимо по контракту.
Repository contractКакой результат и запрет согласованы?Invalid marker должен вернуть fixed invalid-note result.Что diff действительно соблюдает правило.
Reviewer evidenceПочему scope и риск приняты?Path внутри задачи, owner подтвердил расширение.Что сценарий исполнился в среде.
Test evidenceКакой конкретный input-output или side effect наблюдался?Duplicate key даёт error и zero write calls.Что покрыты все callers, права и нагрузки.
UnknownsКакая граница ещё не установлена?Нет real schema, traffic или customer compatibility.Что риска нет.

Короткая последовательность проверки

  1. Отделите candidate. Он не является готовым решением.
  2. Сверьте contract и paths. У changed branch есть allowed и forbidden result, а scope подтверждён owner.
  3. Свяжите test с branch. Назовите input, output и forbidden side effect.
  4. Оставьте unknown. Отсутствие evidence не доказывает security или compatibility.
Матрица четырёх ошибок: scope, contract, test evidence и unknowns. У каждой ошибки показано, почему правдоподобный код не решает её сам и какое evidence нужно получить до merge.
Матрица не классифицирует реальные инциденты. Это компактная модель для review fixed synthetic diff, где каждый столбец отвечает на отдельный вопрос.

Model output: кандидат, а не свидетель

Model output можно читать как черновик: он показывает, какую гипотезу стоит проверить. Он не говорит, какие правила приняты в данном repository, кто владеет последствиями и какой тест подтверждает boundary. Даже подробное объяснение не меняет этого статуса: explanation может хорошо описывать локальный алгоритм и одновременно выдумать смысл отсутствующего поля. Поэтому не нужно спорить, «умный» ли ответ. Нужен более полезный вопрос: каким контрактом и каким evidence мы проверим каждое новое поведение?

Рассмотрим fixed synthetic formatter. В одном видимом случае non-empty note форматируется правильно. Candidate diff возвращает empty string для invalid marker, потому что так проще объединить ветки. Код выглядит коротко, а тест happy path остаётся зелёным. Ошибка не в синтаксисе. Ошибка в том, что модель выбрала семантику за владельца контракта. До merge reviewer должен увидеть запрещённый результат рядом с changed branch, а тест — отличить invalid marker от blank input.

// Fixed synthetic contract, not repository code.
const rows = [
  { input: 'note', expected: 'formatted-note' },
  { input: 'blank', expected: 'absent-note' },
  { input: 'invalid-marker', expected: 'invalid-note' },
];

// A plausible-looking candidate is rejected if it maps 'invalid-marker'
// to '' instead of 'invalid-note'.
// The table describes a contract question; it does not call a model or a real formatter.

Repository contract: правило до реализации

Контракт не обязан быть длинной спецификацией. Для маленького изменения достаточно назвать scope, входы, допустимые выходы, forbidden result и owner. Главное — чтобы contract существовал до того, как diff создаст удобную новую интерпретацию. Если задача допускает изменение контракта, это можно сделать, но тогда в карточке появляется отдельное решение: какие consumers затронуты, кто принимает migration и почему новая форма совместима. Нельзя спрятать это решение в строке, которую ассистент добавил между двумя тестами.

Код может ломать контракт не только возвращаемым значением. Частый случай — скрытый side effect. В teaching case duplicate key должен вернуть fixed error и не вызвать write helper. Candidate способен вернуть верный error после ненужного вызова. Без явного запрета и test evidence reviewer видит только label и считает ветку безопасной. Для таких вещей контракт должен называть не только результат, но и то, чего ветка не делает.

Reviewer evidence: связь между diff и задачей

Review evidence отвечает на вопрос «почему мы принимаем этот scope». В него входят paths, связь с задачей, owner и явное решение о расширении. Тест может пройти для parser и не объяснить access helper; согласие owner на файл не проверяет runtime behavior. GitHub описывает review как просмотр commits, files и diff с исходами comment, approve или request changes. Поэтому запись должна быть точной: «path X вне prompt-card — request changes», а не «looks good».

Test evidence: наблюдать изменённую границу

Наличие теста не равно evidence. Если candidate меняет duplicate branch, а test проверяет unique branch, проверки изменения нет. Для каждой changed branch нужны input, expected output и forbidden side effect.

Матрица ошибок и минимальная реакция
ОшибкаПочему код кажется убедительнымПроверкаДействие
Scope mismatchПолезный hunk соседствует с неназванным файлом.Сверить каждый changed path с prompt-card.Остановить и отделить расширение scope.
Contract mismatchHappy path совпадает с ожиданием.Прочитать fixed invalid и absent rows.Вернуть diff владельцу контракта.
Test mismatchЕсть зелёный test, но для другой ветки.Связать changed branch с input и forbidden effect.Добавить focused negative case.
Unknown treated as passНет явной ошибки или данных о конфликте.Отметить, чего evidence не покрывает.Не обещать security, compatibility или completeness.

Линтер проверяет часть формы, но не знает доменный запрет, если он не закодирован в rule. Unit и integration test наблюдают свои scenarios. Model output добавляет гипотезу. Ни один слой не гарантирует correctness или security для всего system.

Unknowns — не пустая ячейка

Unknown не означает провал review. Он означает, что нельзя назвать risk закрытым без evidence. В synthetic package неизвестны consumers, schema, права, concurrency, deployment и security context; пример не притворяется production investigation. В реальной задаче unknown может потребовать сузить diff, позвать owner или не применять assistant к чувствительной области. Явная граница дешевле ложного обещания compatibility.

Воспроизводимая модель разделения evidence

Ниже используется только fixed in-memory module текущего пакета. Он принимает три заранее заданных case id, строит canonical report и отклоняет extra keys, sparse arrays, forged decisions и cyclic JSON. Это не evaluator модели и не прогон CI. Проверка полезна как защита структуры: красивый report нельзя выдать за другой case, а неявный unknown key не пройдёт в review draft.

import {
  createFixedSyntheticAiCodingInput,
  inspectSyntheticAiCodingAssistant,
  planSyntheticAiCodingReview,
  stopSyntheticAiCodingReview,
  runAiCodingAssistantFixture,
} from './upgrade-2025-01.mjs';

const report = inspectSyntheticAiCodingAssistant(
  createFixedSyntheticAiCodingInput('fixed-test-mismatch-v1'),
);
const draft = planSyntheticAiCodingReview(report);
const stopped = stopSyntheticAiCodingReview(draft);

if (!Object.values(runAiCodingAssistantFixture().assertions).every(Boolean)) {
  throw new Error('fixed synthetic fixture failed');
}

console.log({ decision: report.decision.code, stopped: stopped.stopped });

// Fixed objects in memory only.
// No model call, real prompt, customer code, repository, secret, file, Git, network, CI, clock, production, metric or evaluation result is accessed.
// stopped records a teaching boundary; it is not a branch rollback or production action.

PASS fixture означает только сохранение exact input/report/draft contracts и stop boundary для трёх fixed cases. Он ничего не сообщает о реальной модели, prompts, source code, CI, метриках или production behavior.

Ограничения и следующий проверяемый шаг

Материал не утверждает, что один тест, линтер, review или помощник гарантируют correctness, security или отсутствие регрессий. GitHub рекомендует review и тестирование generated code, но это рекомендация по снижению риска, не сертификат результата. NIST SSDF задаёт рамку для интеграции secure-development practices в SDLC, но не знает доменный контракт вашего сервиса. Все examples и verdicts в статье — fixed synthetic literals без внешнего ввода и IO.

Следующий шаг: в следующем AI-assisted pull request заведите пять явных полей из первой таблицы. Для каждого changed branch заполните хотя бы один contract row и одно test evidence. Затем отдельной строкой напишите unknown. Ожидаемый результат — reviewer сможет либо показать точное несоответствие, либо принять ограниченный diff с названной границей, а не доверять коду потому, что он выглядит готовым.

Историческая граница января 2025

Использованы только источники, существовавшие к январю 2025: immutable GitHub Docs snapshot от 31 января 2025 для limitations, prompts and pull-request review, а также NIST SP 800-218 Version 1.1. В тексте нет поздних model features, современных evaluation claims или корпоративных результатов. Термины model output, contract, review evidence, test evidence и unknowns служат для разделения fixed synthetic data, а не для описания реального pipeline.

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

  • GitHub Docs snapshot: Copilot Chat limitations (immutable GitHub Docs commit 6a92295d, 31 January 2025) — Документация ограничивает область помощника контекстом и прямо предупреждает: код может выглядеть валидным, но не соответствовать намерению разработчика; для чувствительного к безопасности кода нужны review и тестирование. Граница: Это описание ограничений конкретного продукта и интерфейса. Оно не измеряет качество любого помощника, не доказывает корректность конкретного diff и не задаёт процесс merge.
  • GitHub Docs snapshot: improving Copilot Chat performance (immutable GitHub Docs commit 6a92295d, 31 January 2025) — Документация рекомендует держать запрос в рамке задачи, использовать помощник как инструмент, а не замену инженера, и проверять сгенерированный код через secure coding и code review. Граница: Рекомендация не означает, что хороший prompt, линтер или один тест дают гарантию correctness, security или совместимости с конкретным репозиторием.
  • GitHub Docs snapshot: reviewing proposed pull-request changes (immutable GitHub Docs commit 6a92295d, 31 January 2025) — Pull request review рассматривает commits, files и diff, позволяет оставить комментарии, approve или request changes; diff удобно просматривать по файлам. Граница: Документация описывает механизм review в GitHub. Она не утверждает, что просмотренный diff или approval сам по себе доказывает отсутствие дефектов.
  • NIST SP 800-218: Secure Software Development Framework Version 1.1 (final publication, 3 February 2022) — SSDF задаёт набор практик безопасной разработки, которые можно встраивать в конкретный SDLC, чтобы снижать число уязвимостей и влияние невыявленных проблем. Граница: SSDF — высокоуровневая рамка. Он не заменяет знания предметного контракта, тестовые данные, human review или решение владельца об acceptable risk.