{ "index": 43, "slug": "editorial-2026-10-field-code-review-standard", "title": "Code review: как не пропустить риск изменения контракта", "excerpt": "Форматирование занимает строки в комментариях, а изменение контракта остаётся без проверки. Разбираем порядок review, который связывает симптом, evidence, отрицательный путь и решение.", "contentHtml": "
В pull request поле ответа меняют с обязательного на nullable. В обсуждении появляются замечания о названии функции, порядке импортов и длине строки. Через неделю старый клиент получает null и падает в арифметике. Проблема возникла не в синтаксисе: review проверил видимый diff, но не проверил границу контракта. Цена ошибки — откат, срочный выпуск совместимости и поиск всех потребителей в условиях сбоя.
Code review должен отвечать не только на вопрос «понятно ли написан код», но и на вопрос «что изменилось для каждого участника контракта». Для этого reviewer связывает изменение с одним классом риска, проверяемым evidence и разрешённым выводом. Если evidence неполно, сильный вывод нужно остановить. «Выглядит безопасно» не заменяет список потребителей, тест отрицательного пути или описание возврата.
\nСначала опишите старое и новое поведение одним предложением. Например: «Ответ Price теперь допускает discount: null, а клиент должен отличать отсутствие скидки от ошибки». В такой формулировке видны данные, потребитель и новая ветка. Фраза «улучшили модель» для review слишком широка: по ней нельзя выбрать проверку.
Затем назовите границу, которую пересекает diff. Для API это producer, транспорт, schema и consumer. Для фоновой задачи — состояние до операции, событие, состояние после него и эффект повтора. Для входных данных — источник, правило валидации, trust boundary и последствие нарушения. Один и тот же файл может затронуть несколько границ, но для первого вопроса выберите ту, где цена ошибки выше.
\nТакой порядок согласуется с практикой code review, где сначала выясняют назначение изменения, затем смотрят design и functionality, а после — tests, edge cases и контекст. Проверка строк без понимания границы легко превращается в перечень предпочтений. Проверка границы даёт обозримый вопрос: какой потребитель увидит новую форму, какое состояние повторится или какой вход пересечёт доверенную зону.
\nEvidence — не любое вложение в pull request, а артефакт, который отвечает на названный вопрос. Для изменения контракта это обычно schema delta, карта категорий потребителей, примеры старого и нового ответа, тесты совместимости и описание обратного перехода. Каждый пункт должен иметь владельца и понятный результат. Слова «тесты зелёные» недостаточно: нужно указать, какое свойство тест проверяет и на каком входе.
\n| Вопрос | Evidence | Что оно подтверждает | Чего не подтверждает |
|---|---|---|---|
| Что изменилось? | Старая и новая schema | Форму, обязательность и допустимые значения | Поведение каждого клиента |
| Кто читает ответ? | Карта consumer-категорий и места декодирования | Границу поиска потребителей | Совместимость без теста или чтения кода |
| Что будет при старой форме? | Compatibility test с v1 writer и v2 reader | Результат конкретной пары версий | Все комбинации rollout |
| Что будет при новой форме? | Тест старого reader на новом ответе | Поведение выбранного старого потребителя | Потребителей, которых не включили в выборку |
| Как вернуться? | Описание старой формы, порядка и условия отката | Возможный путь возврата | Скорость и успех отката в аварии |
У карты есть полезное свойство: она ограничивает вывод. Schema delta не доказывает, что миграция безопасна. Карта потребителей не доказывает, что каждый потребитель обновлён. Тест одной пары версий не доказывает поведение мобильного приложения, очереди и фонового job одновременно. Reviewer обязан держать эти границы видимыми, иначе список артефактов создаёт ложную уверенность.
\nРассмотрим ответ магазина. В версии v1 скидка всегда была числом. В версии v2 сервер хочет сообщать, что скидка не рассчитана, через null. Это не просто изменение типа. Для клиента нужно определить смысл трёх состояний: поле отсутствует, поле равно null и поле содержит число. Если команда не различает эти состояния, новый ответ может сломать старую логику даже при валидном JSON.
type Price = {\n amount: number;\n discount?: number | null;\n};\n\nfunction total(price: Price): number {\n if (!Object.hasOwn(price, 'discount')) {\n throw new Error('old response: discount is absent');\n }\n\n if (price.discount === null) {\n return price.amount;\n }\n\n return price.amount - price.discount;\n}\n\nconsole.log(total({ amount: 100, discount: 15 })); // 85\nФрагмент проверяет только локальную функцию. Он показывает, что автор сознательно выбрал поведение для отсутствующего поля и для null; он не показывает, как декодер, UI и другие сервисы трактуют тот же ответ. Для воспроизводимости зафиксируйте входы и ожидаемый результат: число 15 даёт 85, null даёт 100, отсутствие поля останавливает функцию с ошибкой. После этого добавьте проверки на старый reader и новый writer, а не ограничивайтесь примером нового кода.
Есть важная асимметрия rollout. Новый reader может научиться принимать старый ответ без поля, но старый reader может не уметь принимать новый null. Поэтому совместимость нужно проверять в обе стороны. Если порядок выпуска допускает встречу новых writers со старыми readers, нужен либо tolerant reader, либо временная форма ответа, либо явный запрет такого порядка. Само слово «nullable» решение не выбирает.
Замечание о стиле может быть полезным, если правило закреплено в style guide или если оно мешает прочитать код. Но style-комментарий не закрывает вопрос о контракте. Google Engineering Practices прямо разделяет технические факты и личные предпочтения, а необязательное улучшение предлагает помечать как nit. В рабочем review это означает два независимых комментария: короткий style-nit и отдельный вопрос о совместимости.
\nБлокирующий комментарий должен содержать наблюдение, риск, evidence и действие. «Похоже, сломается» — гипотеза. «Поле стало nullable, а в formatReceipt значение передаётся в арифметику без ветки; нужен тест на null или подтверждение иной границы» — проверяемый вопрос. Такой комментарий не обвиняет автора и не требует «проверить всё». Он называет один недостающий факт и ожидаемый результат.
| Наблюдение | Слабый вывод | Проверяемый комментарий |
|---|---|---|
| Поле стало nullable | «API теперь опасный» | «Покажите старых readers и их ветку для null; без этого не видна совместимость» |
| Есть retry после timeout | «Повтор безопасен» | «Какой state записан до повтора и почему побочный эффект не создаст дубль?» |
| Добавили проверку входа | «Уязвимость закрыта» | «Какой источник доверенный, какое правило проверяется и что происходит при отказе?» |
| Тест проходит | «Можно merge» | «Какой отрицательный input должен уронить тест и почему он включён?» |
Положительный тест подтверждает один разрешённый вход. Риск часто скрывается в том, что происходит при отказе, повторе или старой версии. Для контрактного изменения отрицательный путь — это старый consumer, отсутствующее поле, неожиданный тип, null или невозможность вернуть прежнюю форму. Для операции — timeout после побочного эффекта и повтор запроса. Для security — недоверенный источник и вход, который проходит поверхностную проверку.
Если обязательное evidence отсутствует, review должно вернуть stop, а не приблизительный approve. Stop — не оценка автора. Это состояние данных: «карта потребителей отсутствует», «неизвестна семантика null» или «не названо условие отката». После появления evidence reviewer повторяет только связанную ветку и не расширяет вывод автоматически.
const required = ['schemaDelta', 'consumerMap', 'negativeCase'];\n\nfunction assessReview(input) {\n const missing = required.filter((name) => !input[name]);\n\n if (missing.length > 0) {\n return {\n status: 'stop-insufficient-evidence',\n missing,\n action: 'request-named-artifact'\n };\n }\n\n return {\n status: 'question-ready',\n action: 'ask-contract-owner-to-confirm-compatibility'\n };\n}\n\nconsole.log(assessReview({\n schemaDelta: 'discount: number -> number | null',\n negativeCase: 'old reader receives null'\n}));\n// { status: 'stop-insufficient-evidence', missing: ['consumerMap'], ... }\nЭта функция не читает pull request, не запускает CI и не принимает решение о слиянии. Её свойство воспроизводимо: при отсутствии consumerMap она возвращает его имя и не объявляет совместимость доказанной. Если поле добавлено, результат становится question-ready, то есть можно задать узкий вопрос владельцу контракта. Это ещё не утверждение, что ответ безопасен.
Не превращайте checklist в одинаковый пакет для каждого diff. Риск выбирает доказательство, а стоимость проверки должна быть соразмерна последствиям.
\nNIST SSDF предлагает рассматривать безопасную разработку как набор практик, которые встраиваются в существующий жизненный цикл, но не предписывает один инструмент или одинаковую реализацию. Документ отдельно подчёркивает зависимость от риска, стоимости, осуществимости и применимости. Поэтому security-вопрос в review нужно передавать компетентному владельцу, если граница выходит за знания reviewer.
\nЭтот порядок не заменяет архитектурное решение, security assessment, нагрузочный тест, миграционный план или правила защищённой ветки. Он не вычисляет severity и не гарантирует, что неизвестный consumer не существует. Карта потребителей имеет границу поиска; её нужно расширять, если меняются репозитории, версии клиентов, очереди или внешние интеграции.
\nУчебный код намеренно мал. Он не моделирует распределённую транзакцию, реальный schema registry, авторизацию, конкурентную запись или rollout нескольких приложений. Для финансового действия добавьте идемпотентность и аудит. Для персональных данных — права доступа, минимизацию и срок хранения. Для публичного API — версию, период совместимости и коммуникацию потребителей.
\nНе каждый diff заслуживает полного пакета. Локальное изменение имени без изменения поведения может пройти через style guide и узкий тест. Но если меняется обязательность поля, порядок побочных эффектов или trust boundary, сокращать evidence до «локально компилируется» нельзя. Состав проверки определяет последствия, а не размер diff.
\nReview готово к передаче решения, когда без догадок видны четыре вещи: симптом и цена ошибки, затронутая граница, evidence для выбранного риска и отрицательный путь. Для каждого незакрытого пункта указан один владелец и одно действие. Формулировка «можно сливать» допустима только в пределах полномочий и правил репозитория; сама evidence map этого разрешения не выдаёт.
\nПеред отправкой итогового комментария задайте себе контрольный вопрос: «Что именно станет наблюдаемым, если моя гипотеза неверна?» Если ответа нет, это ещё не evidence. Если ответ есть, добавьте его в тест, лог, schema или карту потребителей и ограничьте вывод тем, что этот артефакт действительно показывает.
\n