8 lines
25 KiB
JSON
8 lines
25 KiB
JSON
{
|
||
"index": 43,
|
||
"slug": "editorial-2026-10-field-code-review-standard",
|
||
"title": "Code review: как не пропустить риск изменения контракта",
|
||
"excerpt": "Форматирование занимает строки в комментариях, а изменение контракта остаётся без проверки. Разбираем порядок review, который связывает симптом, evidence, отрицательный путь и решение.",
|
||
"contentHtml": "<p>В pull request поле ответа меняют с обязательного на nullable. В обсуждении появляются замечания о названии функции, порядке импортов и длине строки. Через неделю старый клиент получает <code>null</code> и падает в арифметике. Проблема возникла не в синтаксисе: review проверил видимый diff, но не проверил границу контракта. Цена ошибки — откат, срочный выпуск совместимости и поиск всех потребителей в условиях сбоя.</p>\n<p>Code review должен отвечать не только на вопрос «понятно ли написан код», но и на вопрос «что изменилось для каждого участника контракта». Для этого reviewer связывает изменение с одним классом риска, проверяемым evidence и разрешённым выводом. Если evidence неполно, сильный вывод нужно остановить. «Выглядит безопасно» не заменяет список потребителей, тест отрицательного пути или описание возврата.</p>\n<h2>Начните с границы изменения</h2>\n<p>Сначала опишите старое и новое поведение одним предложением. Например: «Ответ <code>Price</code> теперь допускает <code>discount: null</code>, а клиент должен отличать отсутствие скидки от ошибки». В такой формулировке видны данные, потребитель и новая ветка. Фраза «улучшили модель» для review слишком широка: по ней нельзя выбрать проверку.</p>\n<p>Затем назовите границу, которую пересекает diff. Для API это producer, транспорт, schema и consumer. Для фоновой задачи — состояние до операции, событие, состояние после него и эффект повтора. Для входных данных — источник, правило валидации, trust boundary и последствие нарушения. Один и тот же файл может затронуть несколько границ, но для первого вопроса выберите ту, где цена ошибки выше.</p>\n<p>Такой порядок согласуется с практикой code review, где сначала выясняют назначение изменения, затем смотрят design и functionality, а после — tests, edge cases и контекст. Проверка строк без понимания границы легко превращается в перечень предпочтений. Проверка границы даёт обозримый вопрос: какой потребитель увидит новую форму, какое состояние повторится или какой вход пересечёт доверенную зону.</p>\n<h2>Evidence должно отвечать на конкретный вопрос</h2>\n<p>Evidence — не любое вложение в pull request, а артефакт, который отвечает на названный вопрос. Для изменения контракта это обычно schema delta, карта категорий потребителей, примеры старого и нового ответа, тесты совместимости и описание обратного перехода. Каждый пункт должен иметь владельца и понятный результат. Слова «тесты зелёные» недостаточно: нужно указать, какое свойство тест проверяет и на каком входе.</p>\n<div class=\"table-scroll\"><table><caption>Минимальная карта review для изменения контракта</caption><thead><tr><th scope=\"col\">Вопрос</th><th scope=\"col\">Evidence</th><th scope=\"col\">Что оно подтверждает</th><th scope=\"col\">Чего не подтверждает</th></tr></thead><tbody><tr><td>Что изменилось?</td><td>Старая и новая schema</td><td>Форму, обязательность и допустимые значения</td><td>Поведение каждого клиента</td></tr><tr><td>Кто читает ответ?</td><td>Карта consumer-категорий и места декодирования</td><td>Границу поиска потребителей</td><td>Совместимость без теста или чтения кода</td></tr><tr><td>Что будет при старой форме?</td><td>Compatibility test с v1 writer и v2 reader</td><td>Результат конкретной пары версий</td><td>Все комбинации rollout</td></tr><tr><td>Что будет при новой форме?</td><td>Тест старого reader на новом ответе</td><td>Поведение выбранного старого потребителя</td><td>Потребителей, которых не включили в выборку</td></tr><tr><td>Как вернуться?</td><td>Описание старой формы, порядка и условия отката</td><td>Возможный путь возврата</td><td>Скорость и успех отката в аварии</td></tr></tbody></table></div>\n<p>У карты есть полезное свойство: она ограничивает вывод. Schema delta не доказывает, что миграция безопасна. Карта потребителей не доказывает, что каждый потребитель обновлён. Тест одной пары версий не доказывает поведение мобильного приложения, очереди и фонового job одновременно. Reviewer обязан держать эти границы видимыми, иначе список артефактов создаёт ложную уверенность.</p>\n<figure><img src=\"/assets/editorial/2026/frontend-backend-boundary-2026-review-evidence-loop.svg\" alt=\"Диаграмма code review для контракта: намерение проходит через запрос и HTTP-ответ к входу рендера, а расхождение возвращает проверку соответствующей границы\" loading=\"lazy\" /><figcaption>Соседние границы нужно проверять последовательно: намерение не доказывает форму запроса, ответ не доказывает корректность входа в UI.</figcaption></figure>\n<h2>Учебный кейс: nullable-поле</h2>\n<p>Рассмотрим ответ магазина. В версии v1 скидка всегда была числом. В версии v2 сервер хочет сообщать, что скидка не рассчитана, через <code>null</code>. Это не просто изменение типа. Для клиента нужно определить смысл трёх состояний: поле отсутствует, поле равно <code>null</code> и поле содержит число. Если команда не различает эти состояния, новый ответ может сломать старую логику даже при валидном JSON.</p>\n<pre><code>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</code></pre>\n<p>Фрагмент проверяет только локальную функцию. Он показывает, что автор сознательно выбрал поведение для отсутствующего поля и для <code>null</code>; он не показывает, как декодер, UI и другие сервисы трактуют тот же ответ. Для воспроизводимости зафиксируйте входы и ожидаемый результат: число <code>15</code> даёт <code>85</code>, <code>null</code> даёт <code>100</code>, отсутствие поля останавливает функцию с ошибкой. После этого добавьте проверки на старый reader и новый writer, а не ограничивайтесь примером нового кода.</p>\n<p>Есть важная асимметрия rollout. Новый reader может научиться принимать старый ответ без поля, но старый reader может не уметь принимать новый <code>null</code>. Поэтому совместимость нужно проверять в обе стороны. Если порядок выпуска допускает встречу новых writers со старыми readers, нужен либо tolerant reader, либо временная форма ответа, либо явный запрет такого порядка. Само слово «nullable» решение не выбирает.</p>\n<h2>Отделяйте style-комментарий от блокирующего риска</h2>\n<p>Замечание о стиле может быть полезным, если правило закреплено в style guide или если оно мешает прочитать код. Но style-комментарий не закрывает вопрос о контракте. Google Engineering Practices прямо разделяет технические факты и личные предпочтения, а необязательное улучшение предлагает помечать как nit. В рабочем review это означает два независимых комментария: короткий style-nit и отдельный вопрос о совместимости.</p>\n<p>Блокирующий комментарий должен содержать наблюдение, риск, evidence и действие. «Похоже, сломается» — гипотеза. «Поле стало nullable, а в <code>formatReceipt</code> значение передаётся в арифметику без ветки; нужен тест на <code>null</code> или подтверждение иной границы» — проверяемый вопрос. Такой комментарий не обвиняет автора и не требует «проверить всё». Он называет один недостающий факт и ожидаемый результат.</p>\n<table><caption>Как превратить наблюдение в рабочий комментарий</caption><thead><tr><th scope=\"col\">Наблюдение</th><th scope=\"col\">Слабый вывод</th><th scope=\"col\">Проверяемый комментарий</th></tr></thead><tbody><tr><td>Поле стало nullable</td><td>«API теперь опасный»</td><td>«Покажите старых readers и их ветку для <code>null</code>; без этого не видна совместимость»</td></tr><tr><td>Есть retry после timeout</td><td>«Повтор безопасен»</td><td>«Какой state записан до повтора и почему побочный эффект не создаст дубль?»</td></tr><tr><td>Добавили проверку входа</td><td>«Уязвимость закрыта»</td><td>«Какой источник доверенный, какое правило проверяется и что происходит при отказе?»</td></tr><tr><td>Тест проходит</td><td>«Можно merge»</td><td>«Какой отрицательный input должен уронить тест и почему он включён?»</td></tr></tbody></table>\n<h2>Проверяйте отрицательный путь</h2>\n<p>Положительный тест подтверждает один разрешённый вход. Риск часто скрывается в том, что происходит при отказе, повторе или старой версии. Для контрактного изменения отрицательный путь — это старый consumer, отсутствующее поле, неожиданный тип, <code>null</code> или невозможность вернуть прежнюю форму. Для операции — timeout после побочного эффекта и повтор запроса. Для security — недоверенный источник и вход, который проходит поверхностную проверку.</p>\n<p>Если обязательное evidence отсутствует, review должно вернуть stop, а не приблизительный approve. Stop — не оценка автора. Это состояние данных: «карта потребителей отсутствует», «неизвестна семантика <code>null</code>» или «не названо условие отката». После появления evidence reviewer повторяет только связанную ветку и не расширяет вывод автоматически.</p>\n<pre><code>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'], ... }</code></pre>\n<p>Эта функция не читает pull request, не запускает CI и не принимает решение о слиянии. Её свойство воспроизводимо: при отсутствии <code>consumerMap</code> она возвращает его имя и не объявляет совместимость доказанной. Если поле добавлено, результат становится <code>question-ready</code>, то есть можно задать узкий вопрос владельцу контракта. Это ещё не утверждение, что ответ безопасен.</p>\n<h2>Три класса риска, три набора evidence</h2>\n<p>Не превращайте checklist в одинаковый пакет для каждого diff. Риск выбирает доказательство, а стоимость проверки должна быть соразмерна последствиям.</p>\n<ul><li><strong>Contract risk.</strong> Меняется форма или смысл данных. Нужны старая и новая schema, потребители, пары версий и отрицательные значения. Если значение меняет смысл, одной типовой проверки недостаточно.</li><li><strong>Operational risk.</strong> Меняется переход состояния во времени. Нужны state до события, результат частичного выполнения, поведение timeout/retry, идемпотентность и наблюдаемый сигнал. «Есть retry» не доказывает отсутствие дубля.</li><li><strong>Security risk.</strong> Меняется граница доверия или обработка входа. Нужны источник, правило допуска, нормализация, отказ и последствие злоупотребления. Прохождение одного теста не доказывает отсутствие уязвимости.</li></ul>\n<p>NIST SSDF предлагает рассматривать безопасную разработку как набор практик, которые встраиваются в существующий жизненный цикл, но не предписывает один инструмент или одинаковую реализацию. Документ отдельно подчёркивает зависимость от риска, стоимости, осуществимости и применимости. Поэтому security-вопрос в review нужно передавать компетентному владельцу, если граница выходит за знания reviewer.</p>\n<h2>Порядок review, который можно повторить</h2>\n<ol><li>Сформулируйте симптом и цену пропуска: кто увидит неверное поведение, какие данные потеряются и какой откат потребуется.</li><li>Запишите старую и новую форму или состояние. Не называйте изменение «рефакторингом», если оно меняет внешний контракт.</li><li>Выберите один основной класс риска и выпишите минимальное evidence для него.</li><li>Проверьте intent, design и functionality, затем места потребления, тесты, edge cases и документацию, которую затрагивает изменение.</li><li>Сначала прогоните отрицательную ветку: старый consumer, отказ, timeout, недоверенный input или невозможность возврата.</li><li>Каждый вывод привяжите к артефакту. Если доказательство отсутствует, верните stop с точным именем missing evidence.</li><li>Разделите обязательное исправление и style-nit. Не блокируйте изменение личным предпочтением и не закрывайте риск форматированием.</li><li>Передайте оставшийся вопрос владельцу границы: contract owner, автору state machine, security reviewer или другой явно названной роли.</li><li>После ответа проверьте, что вывод не стал сильнее evidence и что новый тест действительно падает на сломанном поведении.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Этот порядок не заменяет архитектурное решение, security assessment, нагрузочный тест, миграционный план или правила защищённой ветки. Он не вычисляет severity и не гарантирует, что неизвестный consumer не существует. Карта потребителей имеет границу поиска; её нужно расширять, если меняются репозитории, версии клиентов, очереди или внешние интеграции.</p>\n<p>Учебный код намеренно мал. Он не моделирует распределённую транзакцию, реальный schema registry, авторизацию, конкурентную запись или rollout нескольких приложений. Для финансового действия добавьте идемпотентность и аудит. Для персональных данных — права доступа, минимизацию и срок хранения. Для публичного API — версию, период совместимости и коммуникацию потребителей.</p>\n<p>Не каждый diff заслуживает полного пакета. Локальное изменение имени без изменения поведения может пройти через style guide и узкий тест. Но если меняется обязательность поля, порядок побочных эффектов или trust boundary, сокращать evidence до «локально компилируется» нельзя. Состав проверки определяет последствия, а не размер diff.</p>\n<h2>Критерий готовности</h2>\n<p>Review готово к передаче решения, когда без догадок видны четыре вещи: симптом и цена ошибки, затронутая граница, evidence для выбранного риска и отрицательный путь. Для каждого незакрытого пункта указан один владелец и одно действие. Формулировка «можно сливать» допустима только в пределах полномочий и правил репозитория; сама evidence map этого разрешения не выдаёт.</p>\n<p>Перед отправкой итогового комментария задайте себе контрольный вопрос: «Что именно станет наблюдаемым, если моя гипотеза неверна?» Если ответа нет, это ещё не evidence. Если ответ есть, добавьте его в тест, лог, schema или карту потребителей и ограничьте вывод тем, что этот артефакт действительно показывает.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://docs.github.com/en/pull-requests/how-tos/review-pull-requests/reviewing-proposed-changes-in-a-pull-request\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: Reviewing proposed changes in a pull request</a> — официально описывает просмотр commits, changed files и diff, а также варианты Comment, Approve и Request changes. Документ не оценивает качество конкретного review.</li><li><a href=\"https://google.github.io/eng-practices/review/reviewer/standard.html\" target=\"_blank\" rel=\"noopener noreferrer\">Google Engineering Practices: The Standard of Code Review</a> — формулирует баланс между улучшением code health и продвижением изменений, а также приоритет технических фактов над предпочтениями. Это руководство Google, не обязательная policy для каждой команды.</li><li><a href=\"https://google.github.io/eng-practices/review/reviewer/looking-for.html\" target=\"_blank\" rel=\"noopener noreferrer\">Google Engineering Practices: What to look for in a code review</a> — перечисляет design, functionality, tests, edge cases, context и необходимость читать каждую строку с учётом исключений. Источник не подтверждает результаты конкретного проекта.</li><li><a href=\"https://doi.org/10.6028/NIST.SP.800-218\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218, Secure Software Development Framework Version 1.1</a> — официальная риск-ориентированная рамка secure software development; прямо указывает, что не все практики применимы одинаково и учитываются risk, cost, feasibility и applicability. Она не заменяет security assessment конкретной системы.</li></ul>"
|
||
}
|