DarkRiDDeR10 мин

Как остановить merge, когда evidence расходится

AIКачество

Самый неприятный generated diff — не тот, который сразу падает. Опаснее diff, где один сигнал говорит «готово», а другой — «граница нарушена». Зелёный happy path закрывает тикет, reviewer видит аккуратный код, но consumer ждёт старое поле, caller получает изменённый объект или пустая роль проходит в protected branch. Цена ошибки — лишний цикл принятия и неясность, кто должен решить исключение; реальные деньги, пользователи и инциденты в этих карточках намеренно отсутствуют.

В такой момент merge нельзя останавливать фразой «что-то не так». Нужна воспроизводимая цепочка: какой contract заявлен, какой fixed diff его оспаривает, какое evidence расходится, что именно блокируется и какой человек имеет право принять риск. Ни линтер, ни test, ни review не дают сами по себе human approval. Они дают материал, на котором владелец может принять решение или отправить diff на доработку.

Case 1. Contract mismatch: число верно, поле неверно

Первый fixed case выглядит безобидно. Mapper получает subtotalCents и taxCents, складывает их и возвращает число. Generated diff выбирает поле total. Happy-path test проверяет, что итог равен ожидаемому числу. Он зелёный, потому что арифметика не менялась. Но declared output требует объект { amountCents: number }, а synthetic consumer читает result.amountCents. Его результат — undefined.

Симптом: два доказательства смотрят на разные вещи. Причина: test name описывает value, но не public shape. Проверка: сравнить contract assertion, consumer-oriented test и reviewer question «было ли отдельно принято переименование?». Действие: оставить merge preparation blocked до одного из двух честных исходов — вернуть amountCents или оформить compatibility decision за границей этого пакета. Нельзя исправить дело тем, что тест начнёт проверять total: тогда он просто закрепит непроверенное решение.

Case 2. Hidden side effect: preview меняет чужой объект

Во втором case generated helper называется preview. Он возвращает нужный rendered text, и проверка результата зелёная. Внутри helper делает draft.status = "normalized". Если input принадлежит caller, это side effect: после preview следующий код видит не исходный draft. Внешний вид правильный, поэтому проблема не находится проверкой, которая смотрит только на строку в ответе.

Симптом: output совпал, state неожиданно изменился. Причина: contract не разделил derived result и ownership input. Проверка: добавить fixed input before/after assertion, static rule на прямое присваивание аргументу и review вопрос «почему preview имеет право менять draft?». Действие: создать derived value, оставить аргумент неизменным или вынести изменение в отдельную named operation. До этого human approval не просится: решение ещё не имеет согласованного evidence.

Case 3. Incomplete test: editor прошёл, отсутствующая роль тоже проходит

Третий case связан с доступом, но не следует называть его доказанной security vulnerability. Fixed contract задаёт небольшой мир: editor разрешён, viewer и отсутствующее значение отклоняются. Generated condition пишет actorRole !== "viewer". Editor действительно проходит; viewer действительно не проходит. Но отсутствие роли тоже достигает allow(). Если suite содержит только editor test, она сообщает ровно один факт: editor path работает. Она не сообщает, что default-deny сохранён.

Симптом: хороший accept case выдаётся за access decision. Причина: negative path не оформлен как контракт. Проверка: запустить fixed viewer и missing-role assertions, прочитать условие как allow-list, а не как «почти deny-list», и спросить reviewer о политике отсутствующего значения. Действие: записать allow-list и добавить оба отрицательных случая. Это не гарантирует безопасность реальной авторизации: здесь нет токенов, tenant boundary, identity provider и threat model. Но это устраняет конкретную дыру в заявленном small contract.

Цепочка evidence для спорного generated diff: от фиксированного контракта через статический результат, тест, review и ручной сценарий к явному решению block или human approval.
Цепочка отделяет наблюдения от решения. Стрелка в stop не делает revert или rollback: она прекращает только подготовку merge для fixed synthetic карточки.
Три fixed synthetic diff cases
CaseЗелёный сигналРасходящееся evidenceЧто блокируетсяСледующее действие
Contract mismatchчисло рассчитаноshape и consumer access требуют amountCentsmerge preparationвернуть поле или вынести compatibility decision
Hidden side effectpreview output совпалinput ownership нарушен присваиваниемhuman approval requestдобавить derived value и input equality check
Incomplete testeditor allowedотсутствующая роль не проверена и проходит условиеacceptance of access ruleнаписать allow-list и negative cases

Компактный прогон evidence chain

Этот пример проходит ровно ту же fixed in-memory цепочку, что описана в трёх cases. Он не читает PR, не запускает test runner, не вызывает модель и не отправляет сообщение человеку. Его цель — проверить форму решения: plan должен оставаться blocked, пока canonical evidence совпадает с зафиксированной карточкой и явно содержит disagreement.

import {
  createFixedSyntheticVerificationInput,
  inspectSyntheticGeneratedDiff,
  buildSyntheticVerificationPlan,
  stopSyntheticMerge,
} from './upgrade-2025-02.mjs';

const report = inspectSyntheticGeneratedDiff(
  createFixedSyntheticVerificationInput('incomplete-test'),
);
const plan = buildSyntheticVerificationPlan(report);
const stopped = stopSyntheticMerge(plan);

console.log(plan.mergeDecision); // blocked-until-independent-evidence-agrees
console.log(stopped.stopped);    // true
console.log(stopped.evidenceChain); // fixed evidence identifiers only

Fixture у script проверяет именно границы формы: exact keys, dense arrays, unknown keys, cycles и canonical comparison. Если report получает лишнее поле, если action array становится sparse, если self-reference попадает в data или if finding меняют после создания report, next plan закрывается. Такая проверка не утверждает, что сериализация решает инженерную задачу. Она защищает учебный механизм от знакомой подмены: внешне похожее evidence уже содержит неподтверждённый факт или действие.

Где проходит граница stop, revert и rollback

Stop в этом материале означает одно: не продолжать подготовку merge, пока evidence расходится. Он ничего не меняет в VCS и не отправляет команду в delivery pipeline. Revert — отдельное решение об отмене конкретного change, для него нужно знать, принят ли change, как устроена история и кто несёт ответственность за обратный diff. Rollback — отдельное решение о восстановлении уже доставленного состояния; ему нужны реальный scope, состояние данных, проверка восстановления и authority. Подменять stop словом rollback опасно: оно создаёт видимость, что путь восстановления уже доказан.

Human approval нужен после того, как команда может сформулировать выбор. Например: «мы сознательно переименовываем public field и публикуем compatibility path» или «мы сохраняем default-deny и покрыли fixed missing-role branch». Approval не должен закрывать пустоту evidence. Если reviewer не может назвать contract, user-visible boundary и residual risk, вопрос ещё не готов к одобрению. В реальном процессе назначение владельца и полномочия зависят от политики команды; synthetic role в этой статье их не заменяет.

Как документировать next action

У хорошего next action пять частей: case, нарушение, evidence, owner question и stop condition. «Починить AI code» — плохой action: непонятно, что исправлять и чем закончить. «Для fixed guard записать allow-list; добавить viewer и missing-role tests; reviewer подтверждает default-deny; до этого merge blocked» — хороший. Он короткий, проверяемый и не обещает безопасность системы. После того как action выполнен в настоящем репозитории, команда всё равно должна заново собрать фактические evidence: учебная карточка не переносит verdict в production.

Шаблон записи спорного diff
ПолеЧто записатьЧто не писать
Contractодна форма, инвариант или owner boundaryобщую фразу «код должен быть качественным»
Evidenceкакой check нашёл или не нашёл риск«всё зелёное» без scope
Decisionblock, revise или вынести на approvalrollback, если ничего не доставлялось
Owner questionкто принимает остаточный риск и на каких данныхимя человека без вопроса и полномочий
Stopточное расхождение, блокирующее mergeоценку вкуса или доверие генератору

Порядок перед передачей на approval

  1. Соберите цепочку. Contract, static result, positive/negative test, review rationale и ручной сценарий должны ссылаться на один scope.
  2. Отделите observation от decision. Запишите, что каждый signal подтверждает, и не называйте его готовым merge verdict.
  3. Сработайте stop. При противоречии не создавайте revert или rollback автоматически; сохраните evidence и блокируйте только preparation.
  4. Сформулируйте вопрос владельцу. Он должен иметь выбор, границу полномочий и недостающее evidence, а не просьбу «одобрить AI».

Почему источники не дают готового verdict

GitHub Docs на фиксированном commit предупреждают, что generated suggestions и AI review могут пропускать ошибки, давать false positive и предлагать небезопасный код; это аргумент за проверку и human review, не за автоматическое отклонение каждого diff. NIST SSDF PW.7 рекомендует проводить review и analysis по правилам организации, фиксировать и triage findings; он не задаёт единственный список gates. OWASP Code Review Guide подчёркивает, что инструменты не заменяют context и человеческое подтверждение; он не обещает, что manual review найдёт всё. Поэтому final verdict принадлежит не источнику и не модели, а человеку с нужными полномочиями и фактическими evidence.

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

Все три cases — fixed synthetic JavaScript literals: contract names, field names, role values, diff fragments, evidence, tests, owners и verdicts придуманы как учебный материал и хранятся в памяти. Пакет не получает prompt, не вызывает AI, не читает source code, Git, CI, logs, telemetry, секреты или production. Отсюда нельзя сделать вывод о реальной уязвимости, качестве модели, поведении пользователей или готовности релиза.

Следующий шаг: для первого diff, где test и review дают разные сигналы, выпишите evidence chain на одной странице. Не начинайте с решения. Сначала обозначьте contract, затем каждое наблюдение и его scope, потом stop. Только после этого сформулируйте вопрос владельцу: approve exception, revise diff или собрать недостающее evidence. Ожидаемый результат — спор о фактах и границах, а не спор о том, насколько убедительно выглядит generated code.

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

В выводах используются только материалы, доступные к февралю 2025: immutable GitHub Docs commit от 12 декабря 2024, NIST SSDF Version 1.1 final 2022 и OWASP Code Review Guide 2.0 2017. Никаких сведений о поздней эволюции моделей, агентов или автоматических approval-практик в эти cases не добавлено.

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

  • GitHub Docs: Responsible use of GitHub Copilot code review (immutable GitHub Docs commit 7b3918e77baf865d1f16bd60e570acea874ee9eb, 12 December 2024) — В документе сказано, что review дополняет, а не заменяет внимательную проверку человеком; подсказка может быть неточной, синтаксически или семантически неверной либо небезопасной. Ограничение: Это документация конкретного preview-инструмента на зафиксированной ревизии. Она не измеряет качество любого другого ассистента и не даёт правила merge для конкретной команды.
  • NIST SP 800-218, Secure Software Development Framework Version 1.1 (NIST SP 800-218 Version 1.1, final 3 February 2022) — PW.7 предлагает выбирать review и/или code analysis по стадии работы, проводить их по secure-coding standard и фиксировать найденные вопросы и рекомендации; результаты тестирования могут быть входом peer review. Ограничение: SSDF задаёт высокоуровневую рамку. Он не выбирает линтер, не доказывает покрытие, не определяет риск конкретного diff и не отменяет локальные полномочия.
  • OWASP Code Review Guide 2.0 (OWASP Code Review Guide 2.0, July 2017 release PDF) — Руководство описывает code review как проверку присутствия и корректного вызова security и logical controls; инструменты полезны для масштаба, но контекст и подтверждение результата остаются задачей человека. Ограничение: Это общее руководство по secure review, а не каталог правил для языка, фреймворка или модели. Оно не обещает, что review найдёт все уязвимости.