diff --git a/editorial/agent-rewrites/041.json b/editorial/agent-rewrites/041.json index a15f71a..e56b32f 100644 --- a/editorial/agent-rewrites/041.json +++ b/editorial/agent-rewrites/041.json @@ -3,5 +3,5 @@ "slug": "editorial-2026-11-mechanism-technology-evaluation", "title": "Как сравнивать технологии, если данных пока мало", "excerpt": "Практический способ отделить наблюдение от предпочтения: как зафиксировать критерии, остановить псевдоточный выбор и подготовить проверяемое решение.", - "contentHtml": "
Команда сравнивает два runtime. В одном демо вариант A отвечает быстрее. В другом варианте B проще выглядит в коде. Через неделю появляется таблица с баллами 8,7 и 7,9. В ней нет версии окружения, размера входа, повторений и стоимости перехода. Симптом ясен: число выглядит точным, но его нельзя связать с конкретной нагрузкой.
\nЦена ошибки приходит после выбора. Команда переносит код на неподходящую границу, тратит время на обучение и поддержку, а затем объясняет сбой «шумом измерения». Вернуться трудно: исходные условия не записали, поэтому никто не знает, какой вывод нужно пересмотреть. Тезис статьи простой: технология не выбирает себя числом. Сначала нужно разделить вопрос, наблюдение, неопределённость и предпочтение.
\nВопрос описывает, что команда хочет узнать. Например: «какой вариант уменьшает работу по миграции при сохранении текущего API?». Наблюдение отвечает, что произошло в конкретных условиях. Это может быть время операции, число ошибок или объём памяти при названном входе.
\nНеопределённость описывает, где наблюдение может не перенестись. Результат зависит от версии, данных, нагрузки и способа измерения. Предпочтение показывает, что команда считает более важным. Вес критерия 40 — это не свойство технологии. Это открытое решение людей, которое можно оспорить.
\nЕсли поставить вес на место наблюдения, таблица скроет пробел. Если поставить одно измерение на место решения, команда выдаст локальный результат за универсальный. Если убрать неопределённость, читатель не поймёт, где действует вывод. Поэтому эти четыре слоя нужно хранить отдельно и проверять по отдельности.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Есть итоговый балл, но нет входных условий | Локальное наблюдение выдали за общий результат | Найти версию, вход, нагрузку и границу операции | Убрать итог и записать недостающие условия |
| Веса появились после демо | Предпочтение подогнали под понравившийся результат | Спросить, кто и до измерения утвердил критерии | Вернуть веса на обсуждение и сохранить объяснение |
| В отчёте написано «одинаковая среда» | Ключевые параметры спрятали в общей фразе | Раскрыть версии, зависимости, входы и пределы времени | Остановить сравнение до явной конфигурации |
| Есть среднее, но нет разброса | Неопределённость потеряли при агрегации | Проверить повторения и правило обработки выбросов | Показать вариацию или назвать её неизвестной |
| Один вариант назван победителем во всех условиях | Компромисс заменили универсальным рейтингом | Проверить, какие критерии ухудшаются у победителя | Описать trade-off и границу применимости |
Слово «производительность» слишком широкое. Оно может означать задержку, пропускную способность, расход памяти или время восстановления. Сначала назовите одну операцию и её границу: например, «время сериализации объекта размером 1 МБ при версии X». Затем укажите, какое решение это наблюдение должно поддержать.
\nПосле этого зафиксируйте вход. Запишите версию runtime, версию зависимостей, тип процессора, размер данных, число повторений и правило очистки окружения. Не используйте формулировки «реальная нагрузка» и «одинаковая машина» без расшифровки. Они создают видимость контроля, но не дают читателю повторить проверку.
\nСреднее без разброса тоже не даёт уверенности. Если один прогон занял 10 мс, а другой 100 мс, запись «среднее 55 мс» скрывает важное свойство системы. Нужны повторения, диапазон или другая заранее выбранная форма описания вариации. Если повторений не было, напишите «не измерено». Это честнее, чем нулевой разброс.
\nВзвешенная матрица помогает сделать спор видимым. Пусть команда оценивает пригодность, стоимость внедрения и эксплуатацию. Она может назначить веса 40, 35 и 25. Числа задают порядок внимания. Они не говорят, что пригодность в 1,6 раза важнее эксплуатации в объективном смысле.
\nКаждый вес должен иметь вопрос-владелец. Для пригодности спросите, какую границу задачи обязан закрыть вариант. Для стоимости внедрения — какие обучение, миграция, документация и обратимость входят в расчёт. Для эксплуатации — кто будет замечать отказ и сколько времени есть на восстановление.
\nСумма 100 удобна как контроль записи. Она не превращает шкалу в физическую величину. Оценка 3 не означает, что вариант в три раза лучше оценки 1. Если шкала порядковая, так и пишите. Её задача — поддержать разговор о приоритетах, а не создать научный вид.
\nНиже — учебный JavaScript-пример. Он не запускает технологии, не читает файлы и не получает данные из среды. Функция проверяет только структуру заранее заданного объекта. Имена вариантов условны. Код показывает отрицательный путь: если конфигурация скрыта, функция не выдаёт победителя.
\nfunction assessEvaluation(input) {\n const required = ['question', 'alternatives', 'criteria', 'measurement'];\n\n if (!required.every((key) => key in input)) {\n return { status: 'stop', reason: 'missing-field' };\n }\n\n const totalWeight = input.criteria.reduce((sum, item) => sum + item.weight, 0);\n const measurementIsVisible = Boolean(\n input.measurement.input &&\n input.measurement.version &&\n input.measurement.repetitions > 0\n );\n\n if (totalWeight !== 100) {\n return { status: 'stop', reason: 'invalid-weight-total' };\n }\n\n if (!measurementIsVisible) {\n return { status: 'stop', reason: 'hidden-measurement' };\n }\n\n return { status: 'ready-for-review', result: 'no-winner' };\n}\n\nconsole.log(assessEvaluation({\n question: 'compare serialization cost for a fixed input',\n alternatives: ['runtime-a', 'runtime-b'],\n criteria: [\n { name: 'fit', weight: 40 },\n { name: 'adoption-cost', weight: 35 },\n { name: 'operation', weight: 25 },\n ],\n measurement: { input: '1 MB JSON', version: 'fixed', repetitions: 0 },\n}));\n// { status: 'stop', reason: 'hidden-measurement' }\nВ примере веса заполнены, но число повторений равно нулю. Поэтому код возвращает stop. Он не угадывает результат и не заменяет отсутствующее измерение единицей. Статус ready-for-review тоже не означает, что технология выбрана. Он означает только, что структура прошла локальные проверки и может перейти к отдельному обсуждению данных.
Это важное ограничение автоматизации. Проверка полей ловит пропуск, но не знает, подходит ли вход реальному пользователю. Она не проверяет корректность бенчмарка, статистический метод, права доступа и стоимость поддержки. Для этого нужны отдельные владельцы и доказательства.
\nСравнение редко даёт вариант, который лучше по всем осям. Быстрый runtime может потребовать больше обучения. Инструмент с простой миграцией может усложнить диагностику. Библиотека с хорошими метриками может ограничить формат данных. Такие отношения образуют границу компромиссов.
\nПоэтому не спрашивайте «кто победил вообще». Спросите, какой компромисс допустим для данной операции. Если миграцию нельзя откатить за рабочее окно, стоимость обратимости получает больший вес. Если операция чувствительна к задержке, важнее зафиксировать задержку именно этой операции, а не пересказывать общий результат демо.
\nПроверяйте чувствительность решения. Измените один вес и посмотрите, меняется ли порядок вариантов. Если небольшое изменение переворачивает вывод, решение хрупкое. Это не доказывает, что оно неверно. Это показывает, что нужно уточнить критерии и границы данных. Без реальных оценок такая проверка остаётся подготовкой, а не доказательством устойчивости.
\nЭтот механизм не выбирает язык, runtime или базу данных. Он не определяет допустимую нагрузку и не выдаёт статистическую значимость. Он не заменяет security review, оценку лицензий, accessibility-проверку, финансовую модель и план отката.
\nУчебная матрица может помочь увидеть пробел, но не создаёт production-результат. Не вставляйте в неё реальные секреты, идентификаторы клиентов и ссылки на закрытые панели. Если нужно сравнить рабочие системы, сначала получите разрешение на данные и опишите протокол. Пока этого нет, допустим только структурный разбор.
\nОтрицательный путь должен оставаться нормальным исходом. Неверный вес возвращает вопрос к критериям. Скрытый input возвращает вопрос к конфигурации. Большой разброс возвращает вопрос к нагрузке и повторениям. Наличие stop не означает провал команды. Оно показывает, что следующий вывод пока нельзя защищать.
\nСравнение готово к решению, когда читатель может назвать операцию, вход, версии, повторения, критерии и веса. Для каждого числа есть источник и правило интерпретации. Для каждого неизвестного поля есть явная отметка и следующий способ проверки. Для каждого варианта описаны сильная сторона, цена внедрения, эксплуатационный риск и условие отката.
\nГотовность не равна строке «выбран вариант B». Она означает, что решение можно оспорить по частям: отдельно проверить вход, отдельно пересмотреть вес, отдельно повторить измерение. Если хотя бы один слой скрыт, корректный результат — stop, а не красивый рейтинг.
\nКоманда сравнивает два runtime для одной операции. В демо вариант A отвечает быстрее, а вариант B проще выглядит в коде. Через неделю появляется таблица с баллами 8,7 и 7,9, но в ней нет версии окружения, размера входа, числа повторений и стоимости перехода. Симптом узнаваем: число выглядит точным, однако его нельзя связать с конкретной нагрузкой.
\nЦена ошибки проявится после выбора. Код окажется на неподходящей границе, обучение и сопровождение займут больше времени, а сбой объяснят «шумом измерения». Вернуться трудно, потому что исходные условия не записали. Поэтому сравнение технологии — не конкурс инструментов и не поиск вечного победителя. Это проверка конкретного решения: что нужно узнать, что действительно измерено, где остаётся неопределённость и какие предпочтения принимает владелец.
\nХороший вопрос задаёт границу решения. Формулировка «какой runtime лучше» не имеет проверяемого ответа. Вопрос «какой вариант уменьшает работу по миграции для сериализации объекта размером 1 МБ, сохраняя текущий формат API и возможность отката» уже указывает операцию, вход, ограничение и ожидаемое действие.
\nРазделите запись на четыре слоя. Вопрос говорит, какое решение предстоит принять. Наблюдение описывает результат конкретного запуска: например, длительность операции, число ошибок или расход памяти. Неопределённость показывает, какие условия могут изменить результат. Предпочтение задаёт приоритет: команда может считать обратимость важнее небольшой разницы в задержке.
\nЭти слои нельзя подменять. Вес 40 не доказывает пригодность варианта. Балл 3 не означает, что инструмент в три раза лучше варианта с баллом 1. Фраза «надёжнее» не заменяет путь отказа и способ его воспроизвести. Если значение неизвестно, его нужно записать как unknown, а не превращать в аккуратный ноль.
До запуска назовите одну операцию и её начало и конец. Для сериализации это может быть время от передачи подготовленного объекта функции до получения строки. Не смешивайте его с чтением файла, сетевым вызовом и записью в лог: тогда измерение отвечает уже на другой вопрос.
\nЗапишите вход в форме, которую можно получить снова: размер и структура данных, кодировка, число элементов, допустимые ошибки. Затем закрепите версию runtime, зависимости, операционную систему, процессор и параметры запуска. «Та же машина» недостаточно, если неясно, менялись ли фоновые процессы, режим энергопотребления или сборщик мусора.
\nОтдельно опишите успех и остановку. Успехом может быть завершение операции без потери данных при заданном лимите времени. Остановкой — исключение, неверный результат, нарушение формата или отсутствие входной конфигурации. Правило остановки не даёт команде компенсировать критический дефект высоким баллом по второстепенному критерию.
\nEvidence — это не любое число в таблице, а число с происхождением и условиями получения. Минимальная запись измерения должна отвечать на пять вопросов: что измеряли, каким входом, в какой версии, сколько раз повторили и как обработали разброс. К ней полезно приложить сырые результаты или ссылку на артефакт, который другой инженер сможет открыть.
\nОфициальные руководства по измерительной неопределённости требуют описывать компоненты, влияющие на результат, а не публиковать только итог. Это не означает, что для каждой внутренней проверки нужна лабораторная методика. Это означает, что команда должна отличать случайное колебание от изменения условия и не называть единичный запуск устойчивым фактом.
\nДокументация Python timeit хорошо показывает практическую ловушку: замер может зависеть от других процессов, поэтому серия запусков полезнее одного значения, а в типичном случае нужно смотреть на весь вектор результатов, а не механически вычислять среднее. Это рекомендация конкретного инструмента, не универсальная статистическая формула. Для своей операции заранее выберите правило: диапазон, квантили, минимум или другой показатель — и объясните, почему он отвечает на вопрос.
Практика воспроизводимых артефактов в ACM добавляет ещё один критерий: результаты должны быть связаны с описанными кодом, данными, версиями и инструкцией запуска. Для инженерного выбора это означает простой тест: сможет ли коллега восстановить условия без устного пояснения автора? Если нет, таблица пока фиксирует мнение, а не проверяемое сравнение.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Есть итоговый балл, но нет входных условий | Локальное наблюдение выдали за общий результат | Найти версию, вход, нагрузку и границу операции | Убрать итог и записать недостающие условия |
| Веса появились после демо | Предпочтение подогнали под понравившийся результат | Сравнить время изменения веса с моментом получения результата | Вернуть веса на обсуждение до нового замера |
| Написано «одинаковая среда» | Конфигурацию спрятали за общей фразой | Раскрыть версии, зависимости, входы и лимиты | Остановить сравнение до явной конфигурации |
| Есть среднее, но нет разброса | Неопределённость потеряли при агрегации | Проверить сырые прогоны и правило обработки вариации | Показать диапазон или назвать значение неизвестным |
| Вариант назван лучшим во всех условиях | Компромисс заменили универсальным рейтингом | Проверить, какие свойства ухудшаются у «победителя» | Описать trade-off и границу применимости |
Стоимость внедрения начинается не с цены лицензии. Для каждой альтернативы перечислите обучение, изменение кода, перенос данных, интеграцию со сборкой и мониторингом, документацию, дежурство и откат. Не подставляйте часы из другого проекта: их можно использовать как гипотезу, но не как факт.
\nФраза «миграция простая» становится проверяемой только после уточнения объёма. Сколько модулей затронуто? Какое окно простоя допустимо? Что считается сохранёнными данными? Можно ли вернуть старую реализацию без ручного исправления записей? Если ответ неизвестен, добавьте отдельное поле и владельца следующей проверки.
\nПолезно считать стоимость не одной суммой, а набором наблюдаемых работ. Тогда выясняется, что вариант с коротким happy path может требовать дорогой диагностики, а вариант с более длинной миграцией — дешёвого и надёжного отката. Число помогает сравнить зафиксированный объём, но не отменяет описания предположений.
\nВзвешенная матрица делает приоритеты видимыми. Например, команда может назначить пригодности вес 40, стоимости перехода 35, эксплуатации 25. Сумма 100 удобна как контроль записи, но не превращает шкалу в физическую величину. Вес отвечает на вопрос «что для нас важнее», а evidence — на вопрос «что мы наблюдали».
\nДля каждого веса задайте вопрос-владелец. Пригодность: какую границу задачи обязан закрыть вариант? Стоимость: какие работы входят и что делает откат обратимым? Эксплуатация: кто заметит отказ, по какому сигналу и за какое время восстановит систему? Если на один критерий нет ответственного и способа проверки, оценка должна остановиться.
\nНе складывайте в один score несовместимые ограничения. Если потеря данных недопустима, это veto-условие, а не минус пять баллов. Если допустимая задержка превышена, высокая оценка документации не делает вариант подходящим. Сначала отсекайте запрещённые состояния, затем сравнивайте оставшиеся компромиссы.
\nНиже приведён самостоятельный пример на JavaScript. Он не запускает runtime, не читает файлы и не объявляет технологию победителем. Функция проверяет структуру плана: обязательные поля, сумму весов, наличие условий измерения и отсутствие неизвестного evidence. В настоящем проекте эти поля должны заполняться из разрешённых артефактов.
\nfunction validateComparison(plan) {\n const required = ['question', 'alternatives', 'criteria', 'measurement'];\n\n if (!required.every((key) => key in plan)) {\n return { status: 'stop', reason: 'missing-field' };\n }\n\n const weights = plan.criteria.reduce(\n (total, criterion) => total + criterion.weight,\n 0,\n );\n const hasMeasurement = Boolean(\n plan.measurement.input &&\n plan.measurement.version &&\n plan.measurement.repetitions > 1 &&\n plan.measurement.rawResults,\n );\n const hasUnknownEvidence = plan.criteria.some(\n (criterion) => criterion.evidence === 'unknown',\n );\n\n if (weights !== 100) {\n return { status: 'stop', reason: 'invalid-weight-total' };\n }\n if (!hasMeasurement || hasUnknownEvidence) {\n return { status: 'stop', reason: 'insufficient-evidence' };\n }\n\n return { status: 'ready-for-review', winner: null };\n}\n\nconst plan = {\n question: 'compare one fixed serialization operation',\n alternatives: ['runtime-a', 'runtime-b'],\n criteria: [\n { name: 'fit', weight: 40, evidence: 'unknown' },\n { name: 'adoption-cost', weight: 35, evidence: 'unknown' },\n { name: 'operations', weight: 25, evidence: 'unknown' },\n ],\n measurement: {\n input: '1 MB JSON',\n version: 'pinned',\n repetitions: 0,\n rawResults: null,\n },\n};\n\nconsole.log(validateComparison(plan));\n// { status: 'stop', reason: 'insufficient-evidence' }\nВ примере сумма весов равна 100, но evidence неизвестен, повторений нет, а сырые результаты отсутствуют. Поэтому результат — stop. Это полезнее, чем выдать случайный score: следующий шаг становится конкретным — описать протокол, получить разрешённые данные и сохранить исходные прогоны.
Валидатор проверяет форму, но не качество самого benchmark. Он не знает, реалистичен ли вход, корректна ли функция измерения, не влияют ли фоновые процессы, соблюдены ли лицензии и можно ли безопасно обработать данные. Такие вопросы требуют отдельной проверки и владельцев. Статус ready-for-review означает только, что план можно обсуждать на следующем уровне; он не разрешает rollout.
Сохраните конфигурацию рядом с результатом: версию runtime и зависимостей, хеш входного набора, параметры команды, число прогревов и повторов, модель оборудования, дату запуска и сырые значения. Если часть окружения нельзя раскрыть, зафиксируйте хотя бы её категорию и причину ограничения. «Локально» — не воспроизводимое описание.
\nРазделяйте повторяемость и переносимость. Повторяемость проверяет, получаем ли мы близкие результаты при тех же условиях. Переносимость спрашивает, сохраняется ли вывод на другой машине, версии или нагрузке. Второй вопрос требует новых измерений. Нельзя объявлять его решённым потому, что один и тот же скрипт дважды дал похожее число.
\nПосле изменения технологии повторите тот же сигнал на той же границе. Если одновременно поменялись вход, версия и способ измерения, это новый эксперимент. Его можно сравнить с прежним только после явного описания отличий. Такая дисциплина защищает от удобного вывода «стало быстрее», когда изменилась сама операция.
\nЭта методика не выбирает технологию автоматически и не создаёт данные из пустой таблицы. Она не заменяет security review, юридическую проверку лицензий, оценку доступности специалистов, accessibility-проверку, финансовую модель или план восстановления. Для критических систем одного учебного benchmark недостаточно.
\nНе переносите результат между разными операциями. Быстрее сериализовать тестовый объект не значит дешевле обслуживать очередь. Удачный запуск на одной машине не доказывает поведение под пиковым входом. Если нагрузка, версии или требования изменились, границу нужно зафиксировать заново.
\nНе используйте в примерах реальные секреты, идентификаторы клиентов и ссылки на закрытые панели. Если данные нельзя законно или безопасно собрать, корректный исход — stop с описанием недостающего доказательства. Отложенное решение лучше, чем рейтинг, который нельзя защитить.
Сравнение готово к решению, когда другой инженер без устного контекста может назвать операцию, вход, версии, повторы, критерии, веса и правила остановки. Для каждого числа видны источник и способ обработки. Для каждого неизвестного указаны владелец и следующий шаг. Для каждого варианта названы сильная сторона, цена внедрения, эксплуатационный риск и условие отката.
\nФинальная проверка проста: временно уберите итоговый столбец и попросите коллегу перечислить, какие факты ещё нужны для выбора. Если разговор возвращается к входу, измерению и ограничениям, матрица помогает принимать решение. Если все защищают уже напечатанного победителя, таблица стала риторикой. Верните её к наблюдаемой проблеме.
\nКоманда выбирает новую технологию по удачному демо. Через несколько недель выясняется, что демо не учитывало миграцию данных, обучение, права доступа и поддержку редкого сбоя. Прототип работает, а основная система ещё не готова его принять. Люди переключаются на ручной разбор, релиз откладывается, а возврат к прежнему решению становится дороже с каждым изменением.
\nЦена ошибки здесь не равна цене лицензии или времени на первый запуск. Она включает работу, которую не записали в сравнении: перенос состояния, изменение контрактов, настройку наблюдения, обучение дежурных и обратный переход. Ошибка становится дорогой ещё и потому, что таблица с итоговым баллом выглядит убедительно. Она скрывает, какие данные измерили, какие предположили и какие критерии добавили после просмотра результата.
\nТезис: сравнение технологии — это проверка решения, а не конкурс инструментов. Хорошая матрица не обещает объективного победителя. Она показывает границу задачи, цену внедрения, эксплуатационный риск, качество evidence и условия, при которых вывод перестаёт действовать.
\nСначала разделите четыре сущности. Вопрос описывает, что нужно узнать. Наблюдение фиксирует то, что действительно получили при заданных условиях. Неопределённость показывает, где результат может измениться. Предпочтение задаёт важность критерия для конкретного решения. Эти сущности связаны, но не заменяют друг друга.
\nВес 35 не доказывает, что переход дешевле. Балл 3 не доказывает, что технология подходит вашему коду. Слово «надёжная» не заменяет границу отказа и способ проверки. Если в клетке нет входных данных или метода измерения, там должно стоять unknown, а не аккуратный ноль. Ноль означает известное плохое свойство. Пустое значение означает, что команда ещё не знает, что именно проверять.
const decision = {\n problem: 'снизить стоимость поддержки очереди',\n alternatives: ['runtime-a', 'runtime-b'],\n criteria: [\n { id: 'fit', weight: 40, evidence: 'unknown' },\n { id: 'adoption', weight: 35, evidence: 'unknown' },\n { id: 'operations', weight: 25, evidence: 'unknown' }\n ],\n status: 'plan-only',\n winner: null\n};\nЭтот фрагмент — учебный пример структуры, а не результат сравнения. Имена альтернатив условны. В нём нет запуска, производственного журнала, benchmark и рекомендации к внедрению. Поле winner: null защищает от перехода от намерения к утверждению. В настоящей системе такой объект должен дополниться владельцем решения, версией входных данных и разрешённым способом получить evidence.
Начинайте не с названия инструмента, а с границы задачи. «Нужна современная платформа» не позволяет проверить результат. «Нужно обрабатывать повторную доставку сообщения с сохранением порядка внутри одного ключа» уже задаёт вопрос к контракту и к отказам. Для каждой альтернативы запишите, какие части задачи она покрывает напрямую, а какие потребуют адаптера, собственной логики или изменения модели данных.
\nПригодность нельзя свести к числу функций в документации. Важен путь ошибки. Если операция прерывается после записи в одно хранилище и до подтверждения в другом, кто обнаружит рассогласование? Можно ли повторить действие без дубликата? Где живёт идентификатор операции? Если технология отвечает только на happy path, её высокий балл создаёт ложное чувство готовности.
\nCost of adoption состоит из нескольких работ. Назовите их отдельно: обучение команды, изменение кода, перенос данных, интеграция с инструментами сборки, наблюдение, документация, дежурство и обратимость. Не нужно сразу превращать список в финансовую модель. Нужно сделать скрытую работу видимой и назначить владельца каждого неизвестного пункта.
\nОсобенно опасна фраза «миграция простая». У неё нет проверяемого смысла, пока не названы объём данных, допустимое окно простоя, схема отката и критерий сохранности. Учебное сравнение может отметить эти поля как unknown. Оно не имеет права подставить среднюю оценку из другого проекта: другая версия, команда или форма данных меняет стоимость перехода.
Технологию будет поддерживать не автор демо, а дежурная команда. Поэтому проверяйте не только пропускную способность, но и обнаружение отказа, восстановление, диагностику и обновление. Уточните, какие метрики доступны, какие события связываются одним идентификатором и что увидит оператор при частичном сбое.
\nФраза «работает стабильно» не является наблюдением. Нужны условия: версия, вход, длительность, нагрузка, число повторов и правило интерпретации. Без них цифра переносится на чужой контекст без основания. Если измерение ещё не разрешено или его конфигурация не описана, корректное действие — остановить сравнение, а не придумать результат.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| У каждой альтернативы есть точный итоговый балл | Неизвестные данные заменили числами | Попросить вход, метод и источник каждого балла | Вернуть ячейку в unknown и остановить итог |
| Победитель меняется после каждого обсуждения | Вес критерия выбран после результата | Сравнить версии матрицы и время изменения веса | Зафиксировать причину веса до новых наблюдений |
| Демо успешно, но миграция не оценена | Проверяли happy path, а не границу перехода | Описать данные, откат, простой и владельца | Добавить отдельный adoption-критерий |
| Оператор узнаёт об отказе от пользователя | Эксплуатацию приняли за наличие метрик | Воспроизвести частичный сбой и пройти alert path | Потребовать сигнал, runbook и срок реакции |
| «Одинаковые условия» нельзя повторить | Конфигурация скрыта в окружении | Проверить версии, входы, повторения и лимиты | Не называть запуск benchmark до фиксации условий |
Вес отвечает на вопрос «насколько этот критерий важен для решения». Evidence отвечает на вопрос «что мы наблюдали и насколько этому можно доверять». Веса 40, 35 и 25 могут быть полезной учебной конфигурацией, если команда явно объяснила приоритеты и понимает, что это не измерение. Они не превращают три неизвестных значения в доказательство.
\nПроведите простую проверку чувствительности только после появления разрешённых данных: измените один вес в заранее заданном диапазоне и посмотрите, меняется ли порядок альтернатив. Если результат меняется от небольшого сдвига, решение зависит от предпочтения и должно так и сообщать. Если результат не меняется, это не доказывает универсальность. Он лишь устойчив к проверенному диапазону при тех же входах.
\nfunction validatePlan(plan) {\n if (plan.criteria.some(item => item.weight == null)) {\n return { status: 'stop-missing-weight' };\n }\n if (plan.criteria.some(item => item.evidence === 'unknown')) {\n return { status: 'stop-no-evidence' };\n }\n if (plan.winner != null && plan.status !== 'measured') {\n return { status: 'stop-unearned-winner' };\n }\n return { status: 'ready-for-review' };\n}\nВалидатор — учебный код. Он не заменяет статистический анализ, архитектурное ревью и контроль доступа. Его задача уже: не дать форме выдать план за измерение. Если отсутствует вес, он возвращает stop-missing-weight. Если evidence неизвестен, он возвращает stop-no-evidence. Если появился победитель без статуса измерения, он возвращает stop-unearned-winner. Положительный статус означает только готовность к следующему ревью, а не разрешение на rollout.
Остановитесь, если критерий не имеет владельца и метода проверки. Остановитесь, если конфигурация измерения скрывает версии, входы или лимиты. Остановитесь, если веса появились после того, как стал виден удобный результат. Остановитесь, если таблица требует назвать победителя, хотя наблюдений нет. Такой stop не означает, что технология плоха. Он означает, что вопрос ещё не готов к честному ответу.
\nЕсть и отрицательный путь после внедрения. Если стоимость поддержки превысила исходное допущение или оператор не может восстановить систему в заданное время, не защищайте первоначальный выбор суммой баллов. Зафиксируйте, какое условие нарушилось, ограничьте дальнейшее распространение решения и проверьте обратимость. Матрица нужна для пересмотра, а не для оправдания уже потраченной работы.
\nМатрица не оценивает всё. Она не заменяет проверку безопасности, лицензий, юридических требований, доступности специалистов и совместимости с конкретной инфраструктурой. Она не создаёт данные, которых нет, и не переносит benchmark из одного окружения в другое. Она также не решает конфликт приоритетов сама: владелец решения должен объяснить, почему одна цена важнее другой.
\nСумма баллов может упростить разговор, но скрывает форму trade-off. Альтернатива с меньшей ценой перехода может требовать больше ручной поддержки. Альтернатива с лучшим happy path может хуже вести себя при восстановлении. Если один критический отказ недопустим, его нельзя компенсировать высокими баллами по второстепенным критериям. Задайте veto-условие отдельно.
\nСравнение готово к решению, когда другой инженер может восстановить его без устного контекста: видит проблему и границу; знает альтернативы; понимает смысл каждого критерия и веса; открывает источник каждого наблюдения; видит версии, входы, повторы и ограничения; может пройти отрицательный путь; знает владельца решения и срок пересмотра. До этого документ готов только к уточнению.
\nПрактическая финальная проверка проста. Удалите итоговый столбец и попросите коллегу ответить, какие данные ещё нужны для выбора. Если ответ не меняется, матрица действительно отделяет вопрос от результата. Если коллега вынужден защищать уже напечатанного победителя, таблица стала риторическим инструментом. Верните её к наблюдаемой проблеме.
\nКоманда выбирает новую технологию по удачному демо. Через несколько недель выясняется, что демо не учитывало миграцию данных, обучение, права доступа и поддержку редкого сбоя. Прототип работает, а основная система ещё не готова его принять. Люди переключаются на ручной разбор, релиз откладывается, а возврат к прежнему решению становится дороже с каждым изменением.
\nЦена ошибки здесь не равна цене лицензии или времени на первый запуск. Она включает работу, которую не записали в сравнении: перенос состояния, изменение контрактов, настройку наблюдения, обучение дежурных и обратный переход. Ошибка становится дорогой ещё и потому, что таблица с итоговым баллом выглядит убедительно. Она скрывает, какие данные измерили, какие предположили и какие критерии добавили после просмотра результата.
\nТезис: сравнение технологии — это проверка решения, а не конкурс инструментов. Хорошая матрица не обещает объективного победителя. Она показывает границу задачи, цену внедрения, эксплуатационный риск, качество evidence и условия, при которых вывод перестаёт действовать.
\nСначала разделите четыре сущности. Вопрос описывает, что нужно узнать. Наблюдение фиксирует то, что действительно получили при заданных условиях. Неопределённость показывает, где результат может измениться. Предпочтение задаёт важность критерия для конкретного решения. Эти сущности связаны, но не заменяют друг друга.
\nВес 35 не доказывает, что переход дешевле. Балл 3 не доказывает, что технология подходит вашему коду. Слово «надёжная» не заменяет границу отказа и способ проверки. Если в клетке нет входных данных или метода измерения, там должно стоять unknown, а не аккуратный ноль. Ноль означает известное плохое свойство. Пустое значение означает, что команда ещё не знает, что именно проверять.
const decision = {\n problem: 'снизить стоимость поддержки очереди',\n alternatives: ['runtime-a', 'runtime-b'],\n criteria: [\n { id: 'fit', weight: 40, evidence: 'unknown' },\n { id: 'adoption', weight: 35, evidence: 'unknown' },\n { id: 'operations', weight: 25, evidence: 'unknown' }\n ],\n status: 'plan-only',\n winner: null\n};\nЭтот фрагмент — учебный пример структуры, а не результат сравнения. Имена альтернатив условны. В нём нет запуска, производственного журнала, benchmark и рекомендации к внедрению. Поле winner: null защищает от перехода от намерения к утверждению. В настоящей системе такой объект должен дополниться владельцем решения, версией входных данных и разрешённым способом получить evidence.
Начинайте не с названия инструмента, а с границы задачи. «Нужна современная платформа» не позволяет проверить результат. «Нужно обрабатывать повторную доставку сообщения с сохранением порядка внутри одного ключа» уже задаёт вопрос к контракту и к отказам. Для каждой альтернативы запишите, какие части задачи она покрывает напрямую, а какие потребуют адаптера, собственной логики или изменения модели данных.
\nПригодность нельзя свести к числу функций в документации. Важен путь ошибки. Если операция прерывается после записи в одно хранилище и до подтверждения в другом, кто обнаружит рассогласование? Можно ли повторить действие без дубликата? Где живёт идентификатор операции? Если технология отвечает только на happy path, её высокий балл создаёт ложное чувство готовности.
\nCost of adoption состоит из нескольких работ. Назовите их отдельно: обучение команды, изменение кода, перенос данных, интеграция с инструментами сборки, наблюдение, документация, дежурство и обратимость. Не нужно сразу превращать список в финансовую модель. Нужно сделать скрытую работу видимой и назначить владельца каждого неизвестного пункта.
\nОсобенно опасна фраза «миграция простая». У неё нет проверяемого смысла, пока не названы объём данных, допустимое окно простоя, схема отката и критерий сохранности. Учебное сравнение может отметить эти поля как unknown. Оно не имеет права подставить среднюю оценку из другого проекта: другая версия, команда или форма данных меняет стоимость перехода.
Технологию будет поддерживать не автор демо, а дежурная команда. Поэтому проверяйте не только пропускную способность, но и обнаружение отказа, восстановление, диагностику и обновление. Уточните, какие метрики доступны, какие события связываются одним идентификатором и что увидит оператор при частичном сбое.
\nФраза «работает стабильно» не является наблюдением. Нужны условия: версия, вход, длительность, нагрузка, число повторов и правило интерпретации. Без них цифра переносится на чужой контекст без основания. Если измерение ещё не разрешено или его конфигурация не описана, корректное действие — остановить сравнение, а не придумать результат.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| У каждой альтернативы есть точный итоговый балл | Неизвестные данные заменили числами | Попросить вход, метод и источник каждого балла | Вернуть ячейку в unknown и остановить итог |
| Победитель меняется после каждого обсуждения | Вес критерия выбран после результата | Сравнить версии матрицы и время изменения веса | Зафиксировать причину веса до новых наблюдений |
| Демо успешно, но миграция не оценена | Проверяли happy path, а не границу перехода | Описать данные, откат, простой и владельца | Добавить отдельный adoption-критерий |
| Оператор узнаёт об отказе от пользователя | Эксплуатацию приняли за наличие метрик | Воспроизвести частичный сбой и пройти alert path | Потребовать сигнал, runbook и срок реакции |
| «Одинаковые условия» нельзя повторить | Конфигурация скрыта в окружении | Проверить версии, входы, повторения и лимиты | Не называть запуск benchmark до фиксации условий |
Вес отвечает на вопрос «насколько этот критерий важен для решения». Evidence отвечает на вопрос «что мы наблюдали и насколько этому можно доверять». Веса 40, 35 и 25 могут быть полезной учебной конфигурацией, если команда явно объяснила приоритеты и понимает, что это не измерение. Они не превращают три неизвестных значения в доказательство.
\nПроведите простую проверку чувствительности только после появления разрешённых данных: измените один вес в заранее заданном диапазоне и посмотрите, меняется ли порядок альтернатив. Если результат меняется от небольшого сдвига, решение зависит от предпочтения и должно так и сообщать. Если результат не меняется, это не доказывает универсальность. Он лишь устойчив к проверенному диапазону при тех же входах.
\nfunction validatePlan(plan) {\n if (plan.criteria.some(item => item.weight == null)) {\n return { status: 'stop-missing-weight' };\n }\n if (plan.criteria.some(item => item.evidence === 'unknown')) {\n return { status: 'stop-no-evidence' };\n }\n if (plan.winner != null && plan.status !== 'measured') {\n return { status: 'stop-unearned-winner' };\n }\n return { status: 'ready-for-review' };\n}\nВалидатор — учебный код. Он не заменяет статистический анализ, архитектурное ревью и контроль доступа. Его задача — не дать форме выдать план за измерение. Если отсутствует вес, он возвращает stop-missing-weight. Если evidence неизвестен, он возвращает stop-no-evidence. Если появился победитель без статуса измерения, он возвращает stop-unearned-winner. Положительный статус означает только готовность к следующему ревью, а не разрешение на rollout.
Остановитесь, если критерий не имеет владельца и метода проверки. Остановитесь, если конфигурация измерения скрывает версии, входы или лимиты. Остановитесь, если веса появились после того, как стал виден удобный результат. Остановитесь, если таблица требует назвать победителя, хотя наблюдений нет. Такой stop не означает, что технология плоха. Он означает, что вопрос ещё не готов к честному ответу.
\nЕсть и отрицательный путь после внедрения. Если стоимость поддержки превысила исходное допущение или оператор не может восстановить систему в заданное время, не защищайте первоначальный выбор суммой баллов. Зафиксируйте, какое условие нарушилось, ограничьте дальнейшее распространение решения и проверьте обратимость. Матрица нужна для пересмотра, а не для оправдания уже потраченной работы.
\nМатрица не оценивает всё. Она не заменяет проверку безопасности, лицензий, юридических требований, доступности специалистов и совместимости с конкретной инфраструктурой. Она не создаёт данные, которых нет, и не переносит benchmark из одного окружения в другое. Она также не решает конфликт приоритетов сама: владелец решения должен объяснить, почему одна цена важнее другой.
\nСумма баллов может упростить разговор, но скрывает форму trade-off. Альтернатива с меньшей ценой перехода может требовать больше ручной поддержки. Альтернатива с лучшим happy path может хуже вести себя при восстановлении. Если один критический отказ недопустим, его нельзя компенсировать высокими баллами по второстепенным критериям. Задайте veto-условие отдельно.
\nСравнение готово к решению, когда другой инженер может восстановить его без устного контекста: видит проблему и границу; знает альтернативы; понимает смысл каждого критерия и веса; открывает источник каждого наблюдения; видит версии, входы, повторы и ограничения; может пройти отрицательный путь; знает владельца решения и срок пересмотра. До этого документ готов только к уточнению.
\nПрактическая финальная проверка проста. Удалите итоговый столбец и попросите коллегу ответить, какие данные ещё нужны для выбора. Если ответ не меняется, матрица действительно отделяет вопрос от результата. Если коллега вынужден защищать уже напечатанного победителя, таблица стала риторическим инструментом. Верните её к наблюдаемой проблеме.
\nВ pull request меняют поле ответа с обязательного на nullable. В комментариях спорят о названии функции, порядке импортов и длине строки. Через неделю старый клиент падает на пустом значении. Ошибка возникла не в синтаксисе. Review проверил видимый diff, но не проверил границу контракта. Цена такого пропуска — аварийный откат, срочный выпуск совместимости и потеря времени у команды, которая теперь ищет всех потребителей вслепую.
\nТезис простой: code review должен связывать каждый существенный риск с проверяемым evidence. Если изменение меняет форму данных, одного чтения строк недостаточно. Нужно назвать потребителей, переходы состояния и путь возврата. Если evidence не хватает, reviewer формулирует точный вопрос и останавливает сильный вывод. Он не заменяет пробел догадкой и не маскирует его стилевым комментарием.
\nСимптом обычно виден в обсуждении: много мелких замечаний, спор о вкусе, длинный список предложений без одного вопроса о поведении системы. Это не доказывает плохой review. Но это сигнал проверить, не вытеснил ли стиль риск. Причина часто лежит за пределами изменённого файла: у поля есть другой consumer, миграция не обратима, а тест покрывает только новый путь.
\nНачните с вопроса: что изменится для пользователя или соседнего сервиса, если этот diff попадёт в основную ветку? Ответ должен быть конкретным. «Станет современнее» не подходит. «Клиент, который не различает null и отсутствие поля, получит другой результат» — подходит. Следующий вопрос: каким артефактом это можно проверить? Это может быть schema delta, карта потребителей, тест на старую форму или явная инструкция отката. Список должен быть конечным.
\nУдобно хранить review как короткую связку из пяти полей: change, risk, evidence, status и next action. Change называет один предмет. Risk описывает тип последствий, а не эмоциональную оценку. Evidence перечисляет входы, которыми можно проверить риск. Status показывает границу текущего вывода. Next action говорит, что должен сделать следующий владелец.
\nchange: fixed-nullable-discount-contract\nrisk: contract-migration\nevidence:\n - fixed-schema-delta\n - fixed-consumer-map\n - fixed-rollback-note\nstatus: evidence-map-ready\nnext: ask-contract-owner-to-confirm-consumers\nИмена в примере учебные. Они не ссылаются на настоящий репозиторий, pull request или production-систему. Их задача — показать форму записи. В реальном review вместо них нужны ссылки на существующие артефакты и владелец каждого из них.
\nЭта модель снижает силу вывода до уровня входных данных. Полная карта потребителей позволяет задать вопрос о совместимости. Она не доказывает, что каждый клиент уже обновлён. Schema delta показывает изменение формы. Она не доказывает, что миграция обратима. Rollback note описывает возможный путь возврата. Он не доказывает, что команда успеет выполнить его в аварии.
\nПредставьте учебный API ответа со скидкой. Было discount: number, стало discount: number | null. Сервер может собрать ответ, а новый тест может пройти. Но старый клиент способен сразу передать значение в арифметику или отрисовать его без ветки для null. Поэтому строка изменения ещё не является достаточным evidence.
type Price = {\n amount: number;\n discount: number | null;\n};\n\nfunction total(price: Price) {\n // Учебный пример: null нельзя молча считать скидкой.\n if (price.discount === null) return price.amount;\n return price.amount - price.discount;\n}\nВ этом фрагменте проверяется только локальное правило функции. Он не проверяет всех клиентов и не показывает результат выпуска. Чтобы review был содержательным, нужно найти границу потребления: кто декодирует ответ, какие значения разрешает его схема, что делает старый код и как тестируется несовместимая форма. Если карты нет, правильный комментарий звучит так: «Нужен список потребителей поля и их поведение при null. Без него нельзя оценить охват изменения». Это вопрос, а не вердикт о качестве автора.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Комментарии заполнены форматированием | Риск поведения не назван | Сверить diff с целью и контрактом | Снять style-only комментарии и задать один вопрос о последствиях |
| Поле стало nullable | Не видны все consumers | Проверить schema delta и карту потребителей | Запросить конкретный список клиентов и обработку null |
| Есть слово rollback | Не описано, что возвращается | Сопоставить старую форму и переход состояния | Попросить шаг возврата и условие его применимости |
| Тест проходит только на новом ответе | Отрицательный путь отсутствует | Подать старую форму и null | Добавить проверку отказа или безопасного значения |
| Автор просит approve при неполном input | Вывод сильнее evidence | Проверить обязательные поля risk-класса | Остановить review с перечнем недостающих данных |
Надёжный стандарт должен объяснять остановку так же ясно, как положительный путь. Если отсутствует consumer map, статус — «недостаточно evidence», а действие — запросить только карту. Не нужно добавлять «вероятно безопасно» или искать потребителей по памяти. Если reviewer видит только изменение стиля, а риск относится к контракту, стилевой комментарий не закрывает проверку. Если risk class неизвестен, сначала нужно назвать его границы.
\nЕсть и другой стоп-сигнал: все обязательные артефакты перечислены, но итоговая фраза говорит «approve and merge». Полный набор входов не превращает учебную карточку в разрешение на слияние. В настоящем процессе approval зависит от полномочий, политики репозитория и результата остальных проверок. В записи review лучше разделять «evidence достаточно для следующего вопроса» и «изменение готово к merge».
\nfunction nextReviewAction(review) {\n if (!review.consumerMap) {\n return { status: 'stop-insufficient-evidence',\n action: 'request-consumer-map' };\n }\n\n if (review.risk === 'contract-migration' && review.decision === 'style-note') {\n return { status: 'stop-style-displaces-risk',\n action: 'request-contract-evidence' };\n }\n\n return { status: 'evidence-map-ready',\n action: 'ask-owner-to-confirm-boundary' };\n}\nКод ограничен учебной проверкой объекта в памяти. Он не читает pull request, не запускает CI и не принимает решение о merge. Его ценность — в явных ветках. Каждая ветка показывает, какое условие отсутствует и что делать дальше. В production-автоматизации те же статусы потребуют отдельного контракта, тестов и владельца.
\nEvidence map не заменяет архитектурное решение, security assessment или эксплуатационную проверку. Он не вычисляет severity, не назначает SLA и не доказывает отсутствие дефекта. Для миграции данных понадобятся отдельные вопросы о совместимости версий, объёме записей и восстановлении. Для security-риска понадобятся trust boundary, правило входа и наблюдаемый сценарий злоупотребления. Нельзя переносить набор полей из одного риска в другой без проверки.
\nСтандарт также не делает review быстрым автоматически. Иногда карта потребителей дороже самого изменения. Это нормальная цена, если поле пересекает границу сервиса. Если изменение локально и контракт не меняется, достаточно меньшего набора evidence. Смысл стандарта не в максимальном числе проверок, а в соразмерности: риск определяет обязательные входы.
\nНе следует превращать каждое замечание в блокирующее. Комментарий о названии может улучшить читаемость, но не должен изображать угрозу совместимости. И наоборот, отсутствие доказательства по контракту нельзя закрывать фразой «потом посмотрим». Разделяйте обязательное условие и полезное предложение.
\nReview готов для передачи решения, когда выполнены четыре условия: цель изменения понятна; риск назван; каждый обязательный вход имеет проверяемый источник; отрицательный путь возвращает явное действие. Дополнительно проверьте, что итоговая формулировка соответствует данным. Если карта потребителей не полна, критерий не выполнен. Если evidence полон, это ещё не равно approval: это означает, что вопрос можно передать владельцу контракта с понятной границей.
\nПрактический тест можно выполнить на учебном объекте. Удалите consumer map — запись должна вернуть stop-insufficient-evidence. Замените проверку риска на style-only — запись должна вернуть stop-style-displaces-risk. Добавьте недопустимое слово approval — запись должна остановиться. Верните все поля и оставьте вывод ограниченным вопросом — запись должна пройти как готовая evidence map. Эти результаты проверяют механику примера, а не production-поведение.
В 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В pull request меняется nullable-поле. Reviewer оставляет шесть комментариев о названиях и форматировании. Никто не спрашивает, какие клиенты читают новое значение, что получит старый клиент и как вернуть прежнюю форму. После слияния один потребитель начинает трактовать null как «скидки нет», а другой — как ошибку. Ошибка появляется не в строке с новым типом. Она появляется в границе контракта.
Цена такого пропуска выше цены неудачного комментария. Команда тратит время на разбор несовместимого ответа, откатывает часть изменений и выясняет, кто владеет обратимостью. При этом review могло выглядеть аккуратно. Проблема не в том, что reviewer не заметил все дефекты. Проблема в том, что вывод оказался сильнее доступных фактов.
\nНадёжное review связывает четыре элемента: наблюдаемый симптом, класс риска, нужное evidence и допустимое действие. Evidence — это проверяемое основание вывода: форма данных, список потребителей, переход состояния или правило входа. Если основание неполно, review останавливает вывод и называет недостающий факт. Такой режим называют fail-closed: пробел не превращается в «скорее всего безопасно».
\nЭта схема не оценивает reviewer и не делает из checklist универсальную policy. Она помогает выбрать следующий вопрос. Изменение формы ответа требует проверить контракт. Новая ветка ошибки требует проверить состояние до и после неё. Проверка входа требует определить границу доверия и возможное злоупотребление. Один комментарий о стиле не закрывает ни одну из этих границ.
\nContract risk возникает, когда меняется форма данных или ожидание потребителя. Назовите старую и новую форму. Затем перечислите категории потребителей. После этого опишите возврат к старой форме или честно укажите, что возврат невозможен.
\nOperational risk возникает, когда меняется поведение во времени. Запишите состояние до ветки, событие, состояние после неё и результат повтора. Слова «retry» и «timeout» сами по себе ничего не доказывают. Нужно показать, повторяется ли побочный эффект и кто увидит отказ.
\nSecurity risk возникает на границе доверия. Назовите доверенную сторону, правило входа и последствие нарушения. Непривычный diff ещё не означает уязвимость. Но отсутствие границы доверия не позволяет утверждать, что проверка входа защищает систему.
\nGate не обязан выдавать approve или reject. Его задача уже выполнена, если он не дал неполному input породить ложное решение. При полном наборе фактов reviewer всё ещё не доказывает работоспособность всей системы. Он получает право сформулировать узкий вопрос: например, «проверьте совместимость этих потребителей с новой формой».
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В обсуждении много style-комментариев, но нет вопроса о данных | Риск границы не назван | Сравнить старую и новую форму, найти потребителей | Остановить вывод и запросить contract evidence |
| Есть обработчик ошибки, но непонятно, что будет при повторе | Не описан переход состояния | Записать state до ветки, событие, state после и эффект повтора | Сформулировать operational question |
| Валидатор принимает вход, но доверие к источнику не определено | Смешаны проверка значения и security boundary | Назвать trust boundary, input rule и abuse consequence | Передать вопрос владельцу безопасности |
| Комментарий говорит «безопасно» после одного теста | Вывод шире evidence | Сверить утверждение с тем, что реально проверил тест | Заменить вердикт на ограниченный результат |
Ниже показана учебная ветка для изменения контракта. Она не читает pull request, репозиторий, CI, сеть или production. Функция получает обычный объект и возвращает статус. Такой пример объясняет механизм stop, но не проверяет совместимость реальных клиентов.
\nconst required = ['schemaDelta', 'consumerMap', 'rollbackNote'];\n\nfunction assessContractEvidence(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-only-named-evidence'\n };\n }\n\n return {\n status: 'contract-question-ready',\n action: 'ask-owner-to-check-compatibility'\n };\n}\n\nconsole.log(assessContractEvidence({\n schemaDelta: 'price: number -> number | null',\n rollbackNote: 'restore previous response before consumer rollout'\n}));\n// missing: ['consumerMap']\nВызов возвращает только имя отсутствующего evidence. Он не делает запрос к клиентам и не сообщает, что совместимость нарушена. Если добавить consumerMap, статус изменится на contract-question-ready. Это тоже не approval. Он лишь разрешает задать владельцу контракта конкретный вопрос.
В реальном review названия полей должны описывать факты проекта. schemaDelta — это не слово «изменился API», а точная старая и новая форма. consumerMap — не список случайных сервисов, а граница поиска и категории потребителей. rollbackNote — не обещание отката, а описание старой формы, порядка возврата и условий, при которых возврат возможен.
Стиль легко обсуждать: строка видна, правило знакомо, исправление локально. Контракт требует контекста. Поэтому style-комментарий не плох сам по себе. Ошибка возникает, когда он закрывает обсуждение изменения поведения. Разделяйте уровни: обязательное правило форматирования исправьте локально, а риск контракта вынесите отдельным блоком с доказательством и владельцем.
\nТо же относится к тесту. Наличие теста не равно проверке нужного свойства. Unit-тест может подтвердить, что функция вернула null. Он не показывает, как это значение трактуют старые потребители. Тест перехода состояния может подтвердить ветку ошибки. Он не доказывает, что повтор не создаёт дубль, если побочный эффект выполняется до записи статуса.
Stop означает, что данных недостаточно даже для точного следующего вопроса. Если неизвестен класс риска, сначала опишите границу изменения. Если для contract risk нет карты потребителей, запросите только её. Не добавляйте «проверьте всё» — такой запрос нельзя проверить и нельзя завершить.
\nEscalation означает, что риск и нужное evidence названы, но решение принадлежит другой роли. Вопрос о форме ответа передают владельцу контракта. Вопрос о повторе побочного эффекта — владельцу состояния или эксплуатации. Вопрос о границе доверия — владельцу безопасности. Передача должна содержать риск, факты, точный вопрос и ограничение вывода. Она не должна приписывать получателю готовый диагноз.
\nСлабый вывод здесь полезнее громкого. «Нужно проверить совместимость потребителей с новой nullable-формой» честнее, чем «все клиенты совместимы». «Нужно уточнить повтор операции после timeout» честнее, чем «retry безопасен». «Нужно привлечь владельца trust boundary» честнее, чем «уязвимость найдена».
\nЭта модель не считает severity, не назначает SLA и не заменяет security-аудит, контрактные тесты, нагрузочные проверки или миграционный план. Она не доказывает отсутствие дефекта. Она ограничивает силу комментария доступными фактами.
\nОдна матрица не покрывает весь домен. Для финансовой операции могут потребоваться идемпотентность и аудит. Для публичного API — версия и период совместимости. Для персональных данных — срок хранения и права доступа. Добавляйте такие строки, когда они принадлежат конкретной границе. Не превращайте review в ритуал, где каждый change получает одинаковый пакет документов.
\nУчебный код выше намеренно мал. Он не моделирует распределённую транзакцию, не запускает миграцию и не возвращает production-метрики. Его проверяемое свойство уже: при отсутствии consumerMap функция возвращает stop и не объявляет совместимость доказанной.
Review готово, когда читатель может ответить на четыре вопроса без догадок: какой симптом привёл к проверке, какая граница риска затронута, какое evidence подтверждает следующий вопрос и какое утверждение запрещено делать. Для неполного набора виден точный stop. Для полного набора есть владелец вопроса и отрицательный путь. Если вместо этих ответов остаются «выглядит хорошо», «тесты зелёные» или «потом проверим», механизм ещё не сработал.
\nВ pull request меняется nullable-поле. Reviewer оставляет шесть комментариев о названиях и форматировании. Никто не спрашивает, какие клиенты читают новое значение, что получит старый клиент и как вернуть прежнюю форму. После слияния один потребитель начинает трактовать null как «скидки нет», а другой — как ошибку. Ошибка появляется не в строке с новым типом, а на границе контракта.
Цена такого пропуска измеряется не количеством комментариев, а временем восстановления: команда разбирает несовместимый ответ, откатывает часть изменений и ищет владельца обратимости. При этом review выглядит аккуратно. Значит, проблема не в недостатке стилистических замечаний, а в том, что итоговый вывод оказался сильнее доступных фактов.
\nНадёжное review связывает четыре элемента: наблюдаемый симптом, класс риска, нужное evidence и допустимое действие. Evidence — проверяемое основание вывода: форма данных, список потребителей, переход состояния или правило входа. Если основание неполно, review останавливает вывод и называет недостающий факт. Это рабочее правило fail-closed: пробел не превращается в «скорее всего безопасно».
\nТакой подход согласуется с двумя официальными ориентирами. GitHub описывает review как просмотр commits, изменённых файлов и diff перед решением approve или request changes. Руководство Google ставит выше личных предпочтений технические факты и данные, а целью review называет улучшение общего состояния кодовой базы. Эти документы не задают одну policy для всех команд, но дают проверяемую границу: комментарий должен помогать оценить изменение, а не только выражать вкус.
\nContract risk появляется, когда меняется форма данных или ожидание потребителя. Зафиксируйте старую и новую форму, перечислите категории потребителей и укажите, как вернуть прежний ответ. Если обратимость невозможна, это должно быть частью решения, а не обещанием в комментарии.
\nOperational risk появляется, когда меняется поведение во времени. Запишите состояние до ветки, событие, состояние после неё и результат повтора. Слова retry и timeout ничего не доказывают сами по себе: нужно показать, повторяется ли побочный эффект и кто увидит отказ.
Security risk появляется на границе доверия. Назовите доверенную сторону, правило входа и последствие нарушения. Непривычный diff ещё не означает уязвимость. Но отсутствие карты доверия не позволяет утверждать, что проверка входа защищает систему.
\nGate не обязан выдавать approve или reject. Его задача уже выполнена, если неполный input не породил ложное решение. При полном наборе фактов reviewer всё ещё не доказывает работоспособность всей системы. Он получает право сформулировать узкий вопрос: «проверьте совместимость этих потребителей с новой формой».
\n| Симптом | Гипотеза о риске | Минимальная проверка | Действие |
|---|---|---|---|
| В обсуждении много style-комментариев, но нет вопроса о данных | Не названа граница контракта | Сравнить старую и новую форму, найти категории потребителей | Остановить вывод и запросить карту совместимости |
| Есть обработчик ошибки, но непонятно, что будет при повторе | Не описан переход состояния | Записать состояние до события, после события и эффект повтора | Задать operational-вопрос владельцу состояния |
| Валидатор принимает вход, но источник доверия не определён | Смешаны проверка значения и security boundary | Назвать trust boundary, правило входа и последствие обхода | Передать точный вопрос владельцу безопасности |
| Комментарий говорит «безопасно» после одного теста | Вывод шире проверенного свойства | Сопоставить утверждение с входами, ветками и потребителями теста | Заменить вердикт на ограниченный результат |
У таблицы есть практическая граница: она не ранжирует severity и не определяет владельца автоматически. Её задача — не потерять первый диагностический шаг. Если в одной строке одновременно появляются три разных риска, разделите их: иначе evidence станет слишком общим и stop снова превратится в «проверьте всё».
\nНиже — самостоятельная функция для проверки полноты входной карты. Она не читает pull request, репозиторий, CI, сеть или production. Запустите её в Node.js, передав изменение схемы, карту потребителей и описание возврата. Код проверяет только наличие трёх полей; он не делает вывод о совместимости клиентов.
\nconst required = ['schemaDelta', 'consumerMap', 'rollbackNote'];\n\nfunction assessContractEvidence(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-only-named-evidence'\n };\n }\n\n return {\n status: 'contract-question-ready',\n action: 'ask-owner-to-check-compatibility'\n };\n}\n\nconsole.log(assessContractEvidence({\n schemaDelta: 'price: number -> number | null',\n rollbackNote: 'restore previous response before consumer rollout'\n}));\n// { status: 'stop-insufficient-evidence',\n// missing: [ 'consumerMap' ],\n// action: 'request-only-named-evidence' }\nРезультат содержит только имя отсутствующего evidence. Функция не обращается к клиентам и не сообщает, что совместимость нарушена. Если добавить consumerMap, статус изменится на contract-question-ready. Это тоже не approval: можно задать владельцу контракта конкретный вопрос, но ответ ещё должен опираться на исходный код, схему или контрактный тест.
Три поля в примере — проектные. schemaDelta означает точную старую и новую форму, consumerMap — границу поиска и категории потребителей, rollbackNote — старую форму, порядок возврата и условия обратимости. В другом проекте минимальный набор будет иным. Важно сохранить правило: каждое требуемое поле должно быть связано с конкретным риском и способом проверки.
Начните с цели изменения и списка затронутых файлов. В документации GitHub отдельный просмотр файлов, комментарии на конкретных изменениях и отметка Viewed помогают не потерять часть diff. Это полезная операционная последовательность, но отметка Viewed не доказывает корректность кода. После неё всё равно нужна проверка свойства, ради которого меняли систему.
\nnull, отказ, повтор или недопустимый вход.Стиль легко обсуждать: строка видна, правило знакомо, исправление локально. Контракт требует контекста. Поэтому style-комментарий не плох сам по себе. Ошибка возникает, когда он закрывает обсуждение изменения поведения. Обязательное правило форматирования исправьте локально, а риск контракта вынесите отдельным блоком с доказательством и владельцем.
\nНаличие теста также не равно проверке нужного свойства. Unit-тест может подтвердить, что функция вернула null, но не показывает, как значение трактуют старые потребители. Тест ветки ошибки может пройти, хотя повтор операции создаёт дубль, если побочный эффект выполняется до записи статуса. Руководство Google отдельно предлагает проверять edge cases и спрашивать, упадёт ли тест при поломке кода. Это хороший фильтр для фразы «тесты зелёные».
Вместо общего комментария оставьте наблюдаемую формулировку: «в ответе поле стало nullable; для клиента A не найдено поведение при null». Такой комментарий содержит изменение, missing evidence и ожидаемого владельца. Он полезнее утверждения «API небезопасен», если проверка ещё не показала нарушение.
Stop означает, что данных недостаточно даже для точного следующего вопроса. Если неизвестен класс риска, сначала опишите границу изменения. Если нет карты потребителей, запросите только её. Не пишите «проверьте всё»: такой запрос нельзя проверить и нельзя завершить.
\nEscalation означает, что риск и нужное evidence названы, но решение принадлежит другой роли. Вопрос о форме ответа передают владельцу контракта, вопрос о повторе побочного эффекта — владельцу состояния или эксплуатации, вопрос о границе доверия — владельцу безопасности. Передача должна содержать риск, факты, точный вопрос и ограничение вывода. Она не должна приписывать получателю готовый диагноз.
\nКорректные формулировки звучат слабее, но дают следующий шаг: «нужно проверить совместимость потребителей с новой nullable-формой», «нужно уточнить повтор операции после timeout», «нужно привлечь владельца trust boundary». Пока нет проверки, нельзя писать «все клиенты совместимы», «retry безопасен» или «уязвимость найдена».
\nМодель не считает severity, не назначает SLA и не заменяет security-аудит, контрактные тесты, нагрузочные проверки или миграционный план. Она не доказывает отсутствие дефекта. Она ограничивает силу комментария теми фактами, которые реально собраны.
\nДля финансовой операции в карту добавьте идемпотентность, аудит и правила сверки. Для публичного API — версию, период совместимости и план удаления старого поля. Для персональных данных — источник согласия, срок хранения и права доступа. Для конкурентного кода — интерливинг, блокировки и наблюдаемое состояние. Эти позиции нужны только там, где они принадлежат затронутой границе.
\nПример с assessContractEvidence намеренно мал. Он не моделирует распределённую транзакцию, не запускает миграцию и не возвращает production-метрики. Его проверяемое свойство уже: при отсутствии consumerMap функция возвращает stop и не объявляет совместимость доказанной. Переносить этот статус на реальные клиенты без отдельной проверки нельзя.
Review готово, когда читатель без догадок отвечает на четыре вопроса: какой симптом привёл к проверке, какая граница риска затронута, какое evidence подтверждает следующий вопрос и какое утверждение запрещено делать. Для неполного набора виден точный stop. Для полного набора есть владелец вопроса, отрицательный путь и критерий результата. Если остаются «выглядит хорошо», «тесты зелёные» или «потом проверим», поведенческий вывод ещё не закрыт.
\nВ pull request может быть двадцать комментариев, но ни одного вопроса о данных, миграции или отказе. Reviewer исправляет имя переменной и форматирование. В это время изменение меняет nullable-поле, порядок переходов состояния или правило доступа. Симптом виден сразу: обсуждение длинное, а главный риск не назван. Цена ошибки — несовместимый потребитель, повторная операция после timeout или доступ к данным не той роли. Такой дефект часто обнаруживается уже после слияния, когда исправление требует обратной миграции или ручного восстановления.
\nCode review снижает риск только тогда, когда связывает границу изменения с проверяемым доказательством. Комментарий должен отвечать на четыре вопроса: что меняется, чем это опасно, какой факт сузит неопределённость и какое действие допустимо сейчас. Если факта не хватает, reviewer должен остановить вывод, а не заполнять пробел догадкой.
\nРазделите изменение на риск-классы. Contract risk возникает, когда меняется форма данных или ожидание потребителя. Operational risk появляется при изменении timeout, retry, состояния, очереди или наблюдаемости. Security risk затрагивает доверенную сторону, правило входа и последствие злоупотребления. Style-only ограничен читаемостью и не меняет поведения.
\nКласс риска не равен severity. Он выбирает первый вопрос. Для изменения контракта нужен schema delta, карта consumers и путь возврата. Для изменения поведения нужны переходы состояния и failure mode. Для границы доступа нужна модель доверия и проверка запрещённого входа. Для локального стиля достаточно короткого объяснения, почему код станет понятнее.
\nEvidence — это именованный факт, который другой инженер может проверить в пределах задачи. Ссылка на файл не всегда является evidence. Три изменённых файла показывают объём diff, но не доказывают, что перечислены все потребители. Тест с зелёным статусом показывает проход конкретного сценария, но не объясняет, что произойдёт при повторе после отказа.
\nСвяжите каждый факт с вопросом. schemaDelta отвечает, какое поле изменилось. consumerMap показывает, кто читает старую форму. rollbackNote описывает, что происходит при возврате. failureMode задаёт отрицательный путь. Такая связь важнее количества ссылок: один точный артефакт может закрыть вопрос, а десять общих ссылок — нет.
Reviewer не обязан принимать формулу «это только рефакторинг». Попросите назвать invariant — свойство, которое не должно измениться, — и способ его проверить. Если invariant не назван, scope остаётся гипотезой. Положительный вывод не открывается.
\nНиже — учебный пример на TypeScript. Он не читает репозиторий и не утверждает результат настоящего review. Функция проверяет только полноту входной карточки. Её задача — не найти дефект автоматически, а не дать написать «можно одобрять», когда отсутствует обязательная граница.
\ntype Risk = 'contract' | 'operational' | 'security' | 'style-only';\n\ntype ReviewCard = {\n risk: Risk;\n evidence: {\n changeBoundary: string;\n question: string;\n verification: string;\n };\n requestedAction: 'comment' | 'stop' | 'handoff';\n};\n\nfunction assess(card: ReviewCard): string {\n const required = [\n card.evidence.changeBoundary,\n card.evidence.question,\n card.evidence.verification\n ];\n\n if (required.some((item) => item.trim() === '')) {\n return 'stop-missing-evidence';\n }\n\n if (card.risk === 'contract' &&\n card.requestedAction === 'handoff') {\n return 'stop-contract-needs-consumer-map';\n }\n\n return card.requestedAction === 'stop'\n ? 'stop-review-question'\n : 'review-question-ready';\n}\n\nconst card: ReviewCard = {\n risk: 'contract',\n evidence: {\n changeBoundary: 'discount is now nullable',\n question: 'which consumers handle null?',\n verification: 'trace each consumer and add the compatibility case'\n },\n requestedAction: 'stop'\n};\n\nconsole.log(assess(card));\n// stop-review-question\nВ примере статус описывает следующий разговор, а не качество кода. Если reviewer не видит карту потребителей, он возвращает stop-contract-needs-consumer-map. Это отрицательный путь. Он полезнее общего комментария «нужно больше тестов», потому что называет недостающий факт и действие. Если карта полна, это всё равно не доказывает совместимость: нужно проверить перечисленные consumers и их обработку null.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Много комментариев о стиле, риск не назван | Все замечания получили один приоритет | Отметить, меняется ли поведение или контракт | Вынести риск в отдельный комментарий |
| «Все клиенты совместимы» без списка | Вывод подменил карту потребителей | Найти владельцев и места чтения старой формы | Остановить вывод и запросить consumer map |
| Тест зелёный, но retry не описан | Проверен happy path | Проследить переход после timeout и повторной попытки | Добавить failure case или оставить stop |
| «Это только рефакторинг» | Не назван invariant | Сравнить вход, выход и побочные эффекты до и после | Попросить invariant и способ проверки |
| Комментарий звучит как приказ, но не объясняет риск | Нормативное слово заменило аргумент | Спросить, какое свойство защищает требование | Переписать комментарий через факт и действие |
Начните с наблюдаемого факта. «Поле discount стало nullable» точнее, чем «изменение опасное». Затем назовите последствие: «клиент, который распаковывает значение без проверки, получит ошибку». После этого укажите проверку: «найдите все consumers старой схемы и покажите обработку null». Завершите действием: «до этой проверки не делаем вывод о совместимости».
Один комментарий должен вести к одному действию. Не смешивайте обязательный вопрос о контракте с необязательным предложением переименовать функцию. Метка request-contract-evidence говорит о границе данных. Метка style-note говорит о читаемости. Автор может ответить на них разными изменениями и не потеряет важный риск среди косметических правок.
Для security и эксплуатации требуйте владельца вопроса, если сами не можете проверить границу. Reviewer может заметить, что endpoint принимает роль из тела запроса, но не должен объявлять всю модель доступа безопасной без контекста авторизации. Точный комментарий выглядит так: «Роль приходит из недоверенного входа. Где сервер связывает её с authenticated user? Нужен путь проверки отрицательного случая». Это уже проверяемый вопрос.
\nМатрица не заменяет тестирование, threat model, дизайн-документ, миграционный план или наблюдаемость. Она не перечисляет всех возможных рисков и не назначает единственный порядок приоритетов. В маленьком style-only изменении запрос consumer map создаст ритуал без пользы. Поэтому классификация тоже должна опираться на invariant и границу поведения.
\nДаже полная карта потребителей не доказывает, что каждый путь проверен. Она показывает область поиска. Результат зависит от статического анализа, динамической маршрутизации, конфигурации и скрытых интеграций. Если список получен неполным способом, так и напишите. Честный stop лучше уверенного «совместимо».
\nСтандарт также не решает спор о продуктовой цели. Изменение может быть технически аккуратным, но не соответствовать требованиям продукта или политики безопасности. В таком случае reviewer фиксирует технические факты и передаёт вопрос владельцу решения. Code review не превращает полномочия reviewer в полномочия архитектора или владельца риска.
\nReview-вопрос готов, если другой инженер может быстро назвать границу изменения, цену ошибки, нужное evidence, отрицательный путь и следующее действие. В тексте нет вывода сильнее, чем подтверждающие факты. Для каждого обязательного замечания указан владелец проверки или понятный способ её выполнить. Косметический комментарий не маскирует контрактный, эксплуатационный или security-риск.
\nПроверьте это на одной карточке. Если читатель не может ответить, какой факт переведёт stop в следующий шаг, карточка не готова. Если ответ есть, это ещё не разрешение на слияние. Это только ясная граница между тем, что уже видно, и тем, что нужно проверить.
\nВ pull request может быть двадцать комментариев, но ни одного вопроса о данных, миграции или отказе. Reviewer исправляет имя переменной и форматирование. В это время изменение меняет nullable-поле, порядок переходов состояния или правило доступа. Симптом виден сразу: обсуждение длинное, а главный риск не назван. Цена ошибки — несовместимый потребитель, повторная операция после timeout или доступ к данным не той роли.
\nХорошее review не обязано находить все дефекты и не обещает идеальный код. Его задача уже выполнена, если оно связывает наблюдаемый факт с границей изменения, проверкой и допустимым действием. Если факта не хватает, reviewer ограничивает вывод: не пишет «совместимо» или «безопасно», а называет, какой вопрос ещё требуется закрыть.
\nНачните с цели изменения, а не с первой строки diff. Что должен получить пользователь, потребитель API или оператор? Затем проверьте дизайн, поведение, сложность, тесты, имена, комментарии, стиль и документацию. Такой порядок совпадает с областями, перечисленными в руководстве Google Engineering Practices, но он не является универсальной политикой: команда может добавить свои требования к миграциям, данным или доступам.
\nРазделите обязательное и необязательное. Нарушенная граница контракта — причина для точного вопроса или остановки. Неудачное имя, которое не меняет смысл и не противоречит style guide, — отдельная рекомендация. Когда оба типа замечаний лежат в одном списке без меток, автор тратит внимание на косметику, а дорогой риск выглядит равным запятой.
\nУ любого diff есть граница, через которую меняется ожидание другой части системы. Для контракта это форма JSON, тип поля, код ошибки или порядок вызовов. Для поведения во времени — состояния, timeout, retry и побочный эффект. Для доступа — доверенная сторона, входное правило и запрещённый результат. Для style-only изменения граница остаётся локальной: меняется читаемость, но не наблюдаемое поведение.
\nКласс риска выбирает следующий вопрос. При nullable-поле сравните старую и новую форму, найдите потребителей и опишите переход. При retry покажите состояние до сбоя, событие, состояние после него и результат повтора. При проверке роли отделите значение из запроса от решения, кто имеет право его передать. Фраза «это только рефакторинг» не закрывает проверку: попросите назвать invariant — свойство, которое должно остаться прежним, — и способ его проверить.
\nEvidence — именованный факт, который другой инженер может проверить в рамках задачи. Ссылка на файл показывает место, но не обязательно показывает всех потребителей. Зелёный тест подтверждает один сценарий, но не объясняет повтор после timeout. Три изменённых файла показывают объём diff, но не доказывают, что откат возможен.
\nПривяжите каждый риск к минимальному набору доказательств. Для контракта это schemaDelta, consumerMap и rollbackNote. Для поведения — stateTransition, failureMode и граница наблюдения. Для доступа — trustBoundary, правило входа и последствие нарушения. Не просите «проверить всё»: такой запрос нельзя завершить и нельзя воспроизвести.
Отделяйте факт от вывода. «Поле стало nullable» — факт. «Все клиенты готовы» — вывод, для которого нужна карта клиентов и проверка их обработки null. «Тест прошёл» — факт о запуске. «Повтор безопасен» — более сильное утверждение, которое требует отрицательного сценария. Если набор неполон, корректный результат — stop с конкретным missing evidence.
| Наблюдаемый симптом | Риск | Минимальная проверка | Допустимое действие |
|---|---|---|---|
| Поле ответа стало nullable | Контракт | Сравнить формы и перечислить потребителей старого типа | Запросить карту потребителей и обработку null |
| После timeout операция повторяется | Поведение во времени | Проследить state до сбоя, повтор и побочный эффект | Оставить вопрос до проверки идемпотентности |
| Роль приходит из тела запроса | Граница доверия | Найти серверную связь роли с authenticated user | Передать узкий вопрос владельцу доступа |
| В обсуждении только форматирование | Риск вытеснен стилем | Проверить цель, вход, выход и invariant | Разделить обязательный риск и style-note |
| Тест зелёный, но проверен только happy path | Ложное покрытие | Сломать условие и убедиться, что тест падает | Добавить отрицательный сценарий или ограничить вывод |
Ниже — небольшой TypeScript-валидатор. Он не читает репозиторий, не запускает CI и не оценивает production. Функция проверяет только полноту карточки: если для контрактного изменения нет карты потребителей, она возвращает stop. Это полезный механизм для шаблона комментария, но не автоматическое разрешение слияния.
\ntype Risk = 'contract' | 'operational' | 'security' | 'style-only';\n\ntype ReviewCard = {\n risk: Risk;\n boundary: string;\n question: string;\n evidence: string[];\n action: 'comment' | 'stop' | 'escalate';\n};\n\nfunction assess(card: ReviewCard) {\n const missing = [\n ['boundary', card.boundary],\n ['question', card.question],\n ['evidence', card.evidence.join(', ')]\n ].filter(([, value]) => value.trim() === '')\n .map(([name]) => name);\n\n if (missing.length > 0) {\n return { status: 'stop-missing-input', missing };\n }\n\n if (card.risk === 'contract' &&\n !card.evidence.includes('consumerMap')) {\n return { status: 'stop-missing-consumer-map', missing: ['consumerMap'] };\n }\n\n return { status: card.action + '-question-ready', missing: [] };\n}\n\nconsole.log(assess({\n risk: 'contract',\n boundary: 'discount: number -> number | null',\n question: 'which consumers handle null?',\n evidence: ['schemaDelta'],\n action: 'stop'\n}));\n// { status: 'stop-missing-consumer-map', missing: ['consumerMap'] }\nПроверяемое свойство примера узкое: при отсутствии consumerMap функция не сообщает о совместимости. Если добавить карту, результат станет stop-question-ready, потому что в вызове выбрано действие stop. Даже тогда карточка не доказывает, что каждый потребитель действительно обработан. Она лишь делает следующий вопрос явным.
Начните с факта: «Поле discount стало nullable». Затем назовите последствие: «старый потребитель может распаковать значение без проверки». Дайте проверку: «покажите список потребителей и тест обработки null». Завершите границей вывода: «до этого нельзя утверждать совместимость». В таком комментарии есть наблюдение, причина запроса и следующее действие.
Один комментарий — одно решение. Обязательное замечание о контракте не прячьте среди предложения переименовать функцию. Для локального стиля используйте явную метку вроде Nit или «необязательно», если это соответствует правилам вашей системы review. В руководстве Google такая маркировка отделяет пожелание от требования; это снижает риск, что автор примет личное предпочтение за блокирующее условие.
Если вопрос требует другой компетенции, передайте его владельцу границы, но не приписывайте ему диагноз. Для безопасности это может быть вопрос о модели доверия, для эксплуатации — о повторе побочного эффекта, для контракта — о совместимости потребителей. Передача должна содержать факты, точный вопрос и известное ограничение. Approval, merge и выпуск — отдельные решения, а не следствие одной заполненной карточки.
\nЭта схема не заменяет дизайн-документ, threat model, контрактные тесты, нагрузочную проверку, аудит доступа или план миграции. Она не доказывает отсутствие уязвимости и не считает severity. Её функция скромнее: не позволить выводу стать сильнее доступных фактов.
\nДля style-only изменения карта потребителей создаст лишнюю процедуру, если граница поведения действительно доказанно не меняется. Для финансовой операции потребуются дополнительные строки об идемпотентности, аудите и сверке. Для публичного API важны версия, период совместимости и план удаления старой формы. Для персональных данных добавьте права, срок хранения и путь удаления. Расширяйте матрицу только теми условиями, которые принадлежат конкретному риску.
\nЕсть и предел статического review. Скрытая динамическая маршрутизация, конфигурация, внешняя интеграция и race condition могут находиться за пределами доступного diff. В этом случае reviewer фиксирует область, которую проверил, и остаточный вопрос. Честный stop полезнее уверенного «безопасно», если подтверждающего эксперимента ещё нет.
\nReview-вопрос готов, когда другой инженер без догадок видит симптом, границу, цену ошибки, нужное evidence, отрицательный путь и следующее действие. Для обязательного замечания понятен владелец проверки. Для style-note ясно, что она не блокирует поведение. Для положительного результата указано, что именно проверено и какое утверждение всё ещё запрещено.
\nПеред отправкой комментария перечитайте его как короткую карточку: факт → риск → доказательство → действие → ограничение. Если в ней осталось «выглядит хорошо», «всё совместимо» или «тесты зелёные» без конкретного свойства, вернитесь к границе изменения. Это не формальность: так обсуждение остаётся воспроизводимым после смены автора и reviewer.
\nПользователь нажимает «Сохранить», видит сообщение об успехе, а после обновления страницы получает старые данные. В DevTools один ответ имеет статус 202, в логе сервера виден 409, а компонент уже переключился в состояние ready. Такой дефект выглядит как одна проблема, но может возникнуть в четырёх местах: намерение превратилось в другой запрос, сервер вернул другой контракт, адаптер потерял ответ или store отрисовал старую версию.
Цена ошибки — не только неверный текст на экране. Пользователь повторяет действие и может создать дубль. Оператор ищет причину в backend, хотя ответ не дошёл до store. Команда добавляет повторный запрос и получает гонку. Каждый следующий workaround увеличивает число состояний, которые нужно объяснять.
Тезис статьи простой: границу frontend и backend нужно проверять по наблюдаемым переходам, а не по месту, где впервые заметили симптом. Сравните intent, request, response и render input. Только после этого выбирайте слой исправления. Если один снимок отсутствует, вывод о причине ещё не доказан.
Взаимодействие проходит несколько границ. UI формирует команду из ввода пользователя. Клиентский слой превращает команду в HTTP-запрос. Gateway или backend возвращает статус, заголовки и representation. Адаптер проверяет ответ и строит view model. Store принимает её с учётом версии и передаёт компоненту. Компонент выбирает данные и рисует экран.
Эти шаги не взаимозаменяемы. HTTP 204 означает успешное выполнение без representation в ответе. Он не доказывает, что компонент уже получил новую read model. HTTP 409 означает конфликт состояния или команды, а не сетевой timeout. Успешное завершение обработчика click не означает, что бизнес-операция завершилась.
Для расследования достаточно безопасных полей: имя операции, класс входа, шаблон маршрута, идентификатор запроса, статус, Content-Type, результат проверки схемы и версия view model. Не нужно писать в лог тело ответа целиком. Идентификатор и хэш нормализованного класса часто связывают события без копирования персональных данных.
| Симптом | Вероятная причина | Проверка | Действие |
|---|---|---|---|
| На экране старое значение, ответ содержит новое | Адаптер или store не принял response | Сравнить response с render input и version | Исправить mapping, cache key или правило принятия версии |
| После повторного клика разные результаты | Гонка ответов или повторная mutation | Записать request id и задержать один ответ в тесте | Ввести idempotency key или отбросить устаревшую версию |
| UI показывает готово, сервер вернул 409 | Клиент считает любой ответ успехом | Проверить status и problem envelope | Разделить transport success и domain rejection |
| Поля исчезли после загрузки | Неверный Content-Type или форма body | Проверить media type и schema validation | Остановить адаптер на невалидном payload |
| curl и браузер дают разные наблюдения | Разные cookie, кеш или render logic | Сопоставить запросы, затем проверить store | Не переносить вывод curl на UI без render input |
Рассмотрим учебный пример. Сервер возвращает JSON с состоянием заказа. UI должен показывать кнопку retry, если синхронизация обязательна. Ошибка появляется, когда обработчик проверяет только факт получения ответа и ставит ready, не разобрав тело.
type OrderScreen = { status: 'ready' | 'blocked'; allowedActions: string[]; messageCode: string; version: number; }; function toScreenModel(response: Response, body: unknown): OrderScreen { if (!response.ok) throw new Error('domain-or-transport-failure'); const value = body as Partial<OrderScreen>; if (value.status !== 'ready' && value.status !== 'blocked') throw new Error('invalid-screen-contract'); return { status: value.status, allowedActions: Array.isArray(value.allowedActions) ? value.allowedActions : [], messageCode: typeof value.messageCode === 'string' ? value.messageCode : 'unknown', version: typeof value.version === 'number' ? value.version : 0 }; }Код показан только как учебная схема. Он не подтверждает поведение конкретного API и не заменяет схему валидации. В реальном приложении не следует молча подставлять version 0, если версия обязательна: лучше остановить переход и отправить безопасный диагностический сигнал.
Store должен принять модель только если она не старше уже принятой. Временная метка не решает задачу: часы процессов могут расходиться, а более поздний ответ может относиться к более раннему чтению. Версия, sequence number или серверное правило порядка дают проверяемое условие.
function accept(current: OrderScreen | undefined, next: OrderScreen) { if (current && next.version < current.version) return current; return next; }Это учебный отрицательный путь: устаревший ответ не меняет экран. Если API не выдаёт версию, не выдумывайте её на клиенте. Сначала определите, допускает ли контракт чтение последнего состояния, нужен ли повторный fetch или достаточно локального подтверждения. Optimistic UI может показать, что нажатие принято. Он не должен выдавать это за подтверждённое состояние ресурса.
Network в браузере показывает запрос и ответ конкретного user agent. Он помогает проверить метод, маршрут, статус, заголовки и тело. Он не показывает, какой объект передали selector или memoized компоненту.
curl повторяет HTTP-обмен с указанными заголовками. Он не воспроизводит cookie policy браузера, отмену запроса при unmount и порядок двух ответов. Лог backend подтверждает обработку на сервере, но не подтверждает, что браузер получил тот же response. Snapshot DOM показывает итог, но не говорит, откуда пришло значение.
Учебная команда для чтения тестового ресурса:
curl --fail-with-body --silent --show-error -H 'Accept: application/json' -H 'X-Request-Id: req-test-42' 'https://api.example.test/orders/42' | jq '{status, allowedActions, messageCode, version}'Здесь фиктивные host и идентификатор. Команда предназначена для чтения тестового ресурса. Не повторяйте mutation, пока не проверили идемпотентность и последствия. Если endpoint требует авторизацию, используйте тестовый токен с ограниченным сроком. Не помещайте секрет в shell history, статью или задачу.
Кеш обычно даёт повторяемость: один и тот же ключ возвращает прежнюю версию. Сравните request key, заголовки кеша, revision и источник данных. Не называйте кеш причиной, пока повторный запрос с новым ключом не меняет наблюдение.
Гонка зависит от порядка. Запрос A ушёл первым, B — вторым, но B вернулся раньше. Если store принимает ответы без проверки версии или актуальности запроса, A перезапишет более новое состояние. В тесте задержите только один ответ. Если результат меняется вместе с задержкой, гипотеза о race получила проверку.
Отдельно проверьте отмену запроса. Компонент мог размонтироваться, adapter мог получить AbortError, а локальный optimistic patch остался. В этом случае отсутствие response не доказывает отказ backend. Оно означает только, что текущий слой не получил наблюдаемого ответа.
Остановитесь, если следующий вывод требует неполученных данных. Так бывает, когда нужен production body с персональными полями, закрытый лог или повторная команда с неизвестным эффектом. Попросите владельца системы дать redacted response, безопасный correlation id или воспроизводимый тестовый запрос.
Остановка — точная граница доказательства. Нельзя объявлять кеш виноватым, если у вас есть только скриншот экрана. Нельзя обвинять backend, если вы не проверили request. Нельзя чинить selector, если render input уже неверен.
Отрицательный путь важен и для автоматической проверки. Невалидный Content-Type должен остановить адаптер. Устаревшая version не должна менять store. 409 должен вести к прикладному сообщению, а не к общему «ошибка сети». Отсутствующий response должен иметь отдельный статус диагностики, а не маскироваться под stale UI.
Протокол не заменяет distributed tracing, contract testing, авторизацию и security review. Он не решает проблему очереди одной HTTP-карточкой: для асинхронной команды нужны message id, статус обработки и правило повторов. Он также не разрешает логировать тело ответа целиком.
Критерий готовности проверяем так: для учебного сценария и отрицательного сценария можно связать intent, request, response и render input по безопасному идентификатору; невалидный ответ не меняет экран; устаревшая версия не перезаписывает новую; 409 получает отдельное прикладное состояние; команда может назвать следующий шаг или остановиться при нехватке данных. Это проверяемое свойство границы, а не обещание production-результата.
Самый опасный дефект на границе frontend и backend выглядит убедительно с обеих сторон. JavaScript показывает покупателю скидку 10%, кнопка становится активной, а сервер при оформлении заказа возвращает полную стоимость. В другом варианте браузер получает ответ 200 и рисует старую скидку из локального состояния. Если сразу переписать условие в одном месте, можно скрыть симптом и оставить два разных правила.
\nНиже — учебный кейс с фиксированными тестовыми данными, а не отчёт о конкретной production-системе. У корзины есть промокод SAVE10. Клиент предварительно показывает скидку для суммы от 5 000 рублей. Backend дополнительно проверяет категорию товара и актуальность корзины. Редкий сценарий: сумма подходит, но один товар исключён из акции. Локальная функция показывает 600 рублей скидки, сервер возвращает решение rejected и ноль.
Главный вывод практический: денежный результат и право применить скидку должны иметь одного авторитетного владельца. Frontend может дать быстрый preview (предварительный расчёт), но не должен выдавать его за подтверждённое состояние. Чтобы найти нарушенную границу, нужно связать намерение пользователя, исходный запрос, ответ сервера, адаптированную модель и вход компонента в рендер. Если один переход не зафиксирован, причина остаётся гипотезой.
\nУ интерфейса есть две разные задачи. Preview отвечает на вопрос «что примерно произойдёт, если текущие условия сохранятся». Подтверждённый расчёт отвечает на вопрос «какую сумму система разрешает использовать в следующей операции». Эти значения могут совпадать, но это не одно и то же поле.
\nВ учебном контракте браузер отправляет состав корзины и код акции. Сервер проверяет цены, доступность, права на акцию, исключения по категориям и текущую ревизию корзины. Ответ содержит сумму в копейках, решение и код причины. Клиент не пересчитывает ответ, а отображает его. Так правило не дублируется в двух исполнителях.
\nОтдельное поле previewDiscountCents полезно только при явно описанном статусе «предварительно». Поле discountCents в подтверждённой модели должно приходить из ответа сервера. Если оба значения отображаются рядом, подпишите источник и момент расчёта, иначе пользователь увидит число без его условий применимости.
Для проверки возьмём корзину из одного товара. Все числа ниже — тестовый fixture (фиксированный набор входов), их можно перенести в unit- или contract-тест. Цена хранится в копейках, чтобы пример не зависел от округления чисел с плавающей точкой.
\nPOST /api/cart/quote\nContent-Type: application/json\nAccept: application/json, application/problem+json\nX-Request-Id: test-quote-046\n\n{\n \"cartRevision\": 12,\n \"promoCode\": \"SAVE10\",\n 'items': [\n { \"sku\": \"A-1\", \"priceCents\": 600000, \"category\": \"gift-card\" }\n ]\n}\n\nHTTP/1.1 200 OK\nContent-Type: application/json\nETag: \"quote-12-7\"\n\n{\n \"cartRevision\": 12,\n \"decision\": \"rejected\",\n \"discountCents\": 0,\n \"totalCents\": 600000,\n \"reasonCode\": \"category-excluded\"\n}\nТестовый сервер должен возвращать один и тот же ответ для этого входа. В браузере неправильная реализация может сначала показать 60 000 копеек, потому что локальное условие видит только сумму. После ответа она обязана заменить preview на discountCents: 0 и показать причину, если такой код предусмотрен интерфейсным контрактом.
Второй прогон меняет только категорию на electronics. Если политика акции разрешает её, ожидаемое решение — accepted, скидка 60 000 и итог 540 000 копеек. Третий прогон меняет cartRevision на устаревшее значение. Он нужен, чтобы отделить расхождение бизнес-правила от конфликта состояния. Нельзя считать эти три случая одной ошибкой «скидка не работает».
| Вход | Ожидаемое решение backend | Что может ошибочно показать UI | Проверка |
|---|---|---|---|
600 000 копеек, gift-card, SAVE10 | rejected, скидка 0 | Скидка 60 000 по локальному порогу суммы | Сопоставить response с render input |
600 000 копеек, electronics, SAVE10 | accepted, скидка 60 000 | Старая модель после смены товара | Проверить cache key и отмену прошлого запроса |
| Устаревшая cartRevision | Конфликт по текущему состоянию | Успех из optimistic UI | Записать порядок запросов и статус ответа |
| Невалидный ответ без decision | Остановка адаптера | Тихая подстановка прежней скидки | Проверить schema validation и отрицательный тест |
Матрица полезна тем, что меняет одну причину за раз. Если одновременно менять промокод, состав корзины и ревизию, положительный или отрицательный результат не подскажет, какая граница нарушена.
\nВ Fetch свойство Response.ok означает только статус из диапазона 200–299. Поэтому response.ok === true не доказывает, что тело содержит именно модель расчёта. Для endpoint, который должен вернуть JSON-котировку, проверяйте ожидаемый статус, Content-Type и обязательные поля отдельно.
Статус 204 означает успешное выполнение без содержимого ответа. Он может быть правильным для команды, после которой клиент сам перечитывает ресурс, но не заменяет JSON-ответ котировки. Статус 202 означает, что запрос принят в обработку, а обработка ещё не завершена; его нельзя трактовать как подтверждённую сумму. Статус 409 описывает конфликт с текущим состоянием целевого ресурса и подходит для отдельного сценария устаревшей корзины, если это согласовано контрактом.
\nКлиентская ветка должна различать транспортную ошибку, отказ доменного правила и невалидную representation (представление ресурса). Сетевой сбой не дал ответа. Отказ промокода дал ответ с понятным решением. Невалидная схема говорит, что текущая версия клиента не может безопасно применить контракт. У всех трёх случаев разный следующий шаг.
\ntype Quote = {\n cartRevision: number;\n decision: 'accepted' | 'rejected';\n discountCents: number;\n totalCents: number;\n reasonCode?: string;\n};\n\nasync function readQuote(response: Response): Promise<Quote> {\n if (!response.ok) {\n throw new Error('quote-http-' + response.status);\n }\n\n if (response.status !== 200) {\n throw new Error('quote-representation-missing');\n }\n\n const contentType = response.headers.get('content-type') || '';\n if (!contentType.includes('application/json')) {\n throw new Error('quote-content-type-invalid');\n }\n\n const value = await response.json() as Partial<Quote>;\n const validDecision = value.decision === 'accepted' || value.decision === 'rejected';\n const validMoney = Number.isInteger(value.discountCents) &&\n Number.isInteger(value.totalCents) && value.discountCents >= 0;\n\n if (!Number.isInteger(value.cartRevision) || !validDecision || !validMoney) {\n throw new Error('quote-schema-invalid');\n }\n\n return value as Quote;\n}\nФрагмент намеренно не делает вывод о конкретном API: он проверяет только локальный адаптер. В production-системе схему нужно синхронизировать с владельцем backend, а коды ошибок — с договорённостью интерфейса. Если обязательное поле отсутствует, безопаснее остановить переход и показать нейтральное состояние, чем сохранить старую скидку, будто она подтверждена.
\nНачните с intent — намерения пользователя: товар выбран, промокод введён, пользователь нажал «Рассчитать». Затем сохраните нормализованный request: метод, шаблон маршрута, безопасный идентификатор запроса, ревизию корзины и хэш набора SKU. Секреты, полные персональные данные и платёжные реквизиты в диагностический контекст не входят.
\nТретий снимок — response. Зафиксируйте статус, Content-Type, коды решения, ревизию и ETag, если сервер его отдаёт. Не нужно копировать тело целиком: для расследования достаточно разрешённого набора полей. Четвёртый снимок — render input, то есть объект, который действительно получил selector или компонент. Network-панель сама по себе не показывает этот объект.
Если response содержит discountCents: 0, а render input содержит 60 000, ищите ошибку в адаптере, store, cache key или optimistic patch. Если render input уже равен нулю, а экран показывает 60 000, переходите к selector, memoization, локальному state или hydration. Если request не содержит категорию, backend не обязан восстановить её из догадки клиента: это дефект request contract.
Для каждого снимка используйте один корреляционный ключ, например test-quote-046. Это не стандарт HTTP и не замена распределённой трассировке, а договорённость диагностического сценария. Ключ связывает события, но не доказывает причинность: порядок и содержимое переходов всё равно нужно проверить.
Кеш даёт обычно повторяемый результат для одного ключа: тот же запрос получает ту же старую representation. Для проверки сравните URL, параметры, заголовки кеша, ревизию и ETag. Новый URL с добавленным случайным параметром — плохой диагностический инструмент, если он меняет контракт и не отражает настоящий путь приложения.
\nГонка зависит от порядка. Запрос A отправлен для electronics, запрос B — после изменения товара для gift-card. Если B вернулся первым, а A пришёл позже, устаревший A может перезаписать store. Искусственно задержите только один ответ в тестовом сервере и запишите последовательность. Если результат меняется вместе с задержкой, гипотеза о race получила воспроизводимую проверку.
ETag и If-Match решают другой класс задачи. HTTP определяет ETag как валидатор representation, а If-Match позволяет условно выполнять изменение и предотвращать потерянную запись. Это полезно для обновления корзины или применения команды, но не превращает любой локальный cartRevision в HTTP-валидатор. Сопоставьте оба поля только после явного описания их владельца и жизненного цикла.
При несовпадении ревизии сервер может вернуть 412 для проваленной предварительной проверки или 409 для конфликта состояния — точный выбор задаёт контракт. Клиент должен показать действие: перечитать корзину, пересчитать котировку или попросить повторить после подтверждения. Автоматический повтор команды с неизвестной идемпотентностью может создать второй побочный эффект.
\nok.Если правило меняется часто, храните условия акции в одном контракте или сервисе, а не копируйте их в два языка. Если быстрый preview нужен для отзывчивости, верните ему ограниченный статус и замените его подтверждённой котировкой после ответа. Выигрыш в скорости интерфейса не оправдывает два независимых источника суммы.
\nДля прикладного отказа API может использовать формат Problem Details с медиа-типом application/problem+json. RFC 9457 описывает поля вроде type, title, detail и расширения для конкретного типа проблемы. Такой формат помогает адаптеру отличить ожидаемый отказ промокода от транспортной ошибки, но не диктует тексты для UI и не разрешает раскрывать внутренние детали.
HTTP/1.1 409 Conflict\nContent-Type: application/problem+json\n\n{\n \"type\": \"https://api.example.test/problems/cart-revision\",\n \"title\": \"Cart changed\",\n \"detail\": \"Refresh the cart before calculating the quote\",\n \"instance\": \"/requests/test-quote-046\",\n \"cartRevision\": 11,\n \"expectedRevision\": 12\n}\nАдрес api.example.test в примере фиктивный. Настоящий type URI должен принадлежать владельцу API и иметь документированное значение. Поля detail и instance нельзя без фильтра показывать пользователю или отправлять в общий лог: они могут содержать идентификаторы, внутренние маршруты или данные, которые не нужны для принятия решения.
На frontend полезно иметь явное отображение: «Корзина изменилась — обновите расчёт», «Промокод не действует для выбранного товара» и «Не удалось получить расчёт». Это разные действия. Общий toast «ошибка сети» заставит пользователя повторять запрос, хотя сервер уже вернул корректный отказ по бизнес-условию.
\nЭтот способ подходит для синхронного HTTP-расчёта, где можно получить request, response и render input. Он не решает сам по себе асинхронную обработку очередью: для неё нужен отдельный статус операции, политика повторов и источник истины о завершении. Он также не заменяет авторизацию, аудит денежных операций, contract testing или проверку округления на сервере.
\nНельзя переносить правило «backend всегда прав» на отображение, которое сознательно является предварительным прогнозом. Preview может быть полезен, если пользователь видит его статус, а окончательная операция повторно проверяет условия. Нельзя считать ETag защитой от повторной покупки, если endpoint не описывает идемпотентность команды. Нельзя использовать тестовые идентификаторы и фиктивные type URI как production-конфигурацию.
\nЕсли нет доступа к телу ответа или к безопасному воспроизведению, остановите сильный вывод. Скриншот показывает симптом, но не доказывает источник числа. Один лог backend показывает обработку запроса, но не доказывает, какой объект получил компонент. В таком случае запросите обезличенный response, request id и минимальный тестовый fixture, а не исправляйте случайный слой.
\nИсправление границы готово, когда fixture с исключённой категорией и fixture с разрешённой категорией дают разные, ожидаемые решения; подтверждённая сумма приходит из одного владельца; response и render input можно связать безопасным идентификатором; устаревший ответ не меняет новую модель; невалидный контракт не сохраняет прежнюю скидку; после обновления страницы отображается то же подтверждённое состояние.
\nЭто проверяемый критерий, а не обещание отсутствия всех ошибок. Перед выпуском отдельно проверьте денежное округление, права на акцию, кеширование, повтор команды и наблюдаемость. Если хотя бы одно из этих условий не входит в тестовый стенд, зафиксируйте границу результата и не называйте локальный прогон доказательством всей системы.
\n