From f13b9014b525402e61ba99c738ac77079efa9985 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 13:29:16 +0300 Subject: [PATCH] editorial: revise articles 041-046 to 10/10 --- editorial/agent-rewrites/041.json | 2 +- editorial/agent-rewrites/042.json | 4 ++-- editorial/agent-rewrites/043.json | 4 ++-- editorial/agent-rewrites/044.json | 2 +- editorial/agent-rewrites/045.json | 2 +- editorial/agent-rewrites/046.json | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) 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

Четыре сущности, которые нельзя смешивать

\n

Вопрос описывает, что команда хочет узнать. Например: «какой вариант уменьшает работу по миграции при сохранении текущего API?». Наблюдение отвечает, что произошло в конкретных условиях. Это может быть время операции, число ошибок или объём памяти при названном входе.

\n

Неопределённость описывает, где наблюдение может не перенестись. Результат зависит от версии, данных, нагрузки и способа измерения. Предпочтение показывает, что команда считает более важным. Вес критерия 40 — это не свойство технологии. Это открытое решение людей, которое можно оспорить.

\n

Если поставить вес на место наблюдения, таблица скроет пробел. Если поставить одно измерение на место решения, команда выдаст локальный результат за универсальный. Если убрать неопределённость, читатель не поймёт, где действует вывод. Поэтому эти четыре слоя нужно хранить отдельно и проверять по отдельности.

\n
\"Граница
Схема показывает пространство компромиссов. Точка победителя не появляется без согласованных входов, измерения и правил интерпретации.
\n

Таблица диагностики: симптом → причина → проверка → действие

\n
Проверка инженерного сравнения до выбора технологии
СимптомПричинаПроверкаДействие
Есть итоговый балл, но нет входных условийЛокальное наблюдение выдали за общий результатНайти версию, вход, нагрузку и границу операцииУбрать итог и записать недостающие условия
Веса появились после демоПредпочтение подогнали под понравившийся результатСпросить, кто и до измерения утвердил критерииВернуть веса на обсуждение и сохранить объяснение
В отчёте написано «одинаковая среда»Ключевые параметры спрятали в общей фразеРаскрыть версии, зависимости, входы и пределы времениОстановить сравнение до явной конфигурации
Есть среднее, но нет разбросаНеопределённость потеряли при агрегацииПроверить повторения и правило обработки выбросовПоказать вариацию или назвать её неизвестной
Один вариант назван победителем во всех условияхКомпромисс заменили универсальным рейтингомПроверить, какие критерии ухудшаются у победителяОписать trade-off и границу применимости
\n

Измерение начинается с вопроса

\n

Слово «производительность» слишком широкое. Оно может означать задержку, пропускную способность, расход памяти или время восстановления. Сначала назовите одну операцию и её границу: например, «время сериализации объекта размером 1 МБ при версии X». Затем укажите, какое решение это наблюдение должно поддержать.

\n

После этого зафиксируйте вход. Запишите версию runtime, версию зависимостей, тип процессора, размер данных, число повторений и правило очистки окружения. Не используйте формулировки «реальная нагрузка» и «одинаковая машина» без расшифровки. Они создают видимость контроля, но не дают читателю повторить проверку.

\n

Среднее без разброса тоже не даёт уверенности. Если один прогон занял 10 мс, а другой 100 мс, запись «среднее 55 мс» скрывает важное свойство системы. Нужны повторения, диапазон или другая заранее выбранная форма описания вариации. Если повторений не было, напишите «не измерено». Это честнее, чем нулевой разброс.

\n

Вес критерия — договорённость, а не метрика

\n

Взвешенная матрица помогает сделать спор видимым. Пусть команда оценивает пригодность, стоимость внедрения и эксплуатацию. Она может назначить веса 40, 35 и 25. Числа задают порядок внимания. Они не говорят, что пригодность в 1,6 раза важнее эксплуатации в объективном смысле.

\n

Каждый вес должен иметь вопрос-владелец. Для пригодности спросите, какую границу задачи обязан закрыть вариант. Для стоимости внедрения — какие обучение, миграция, документация и обратимость входят в расчёт. Для эксплуатации — кто будет замечать отказ и сколько времени есть на восстановление.

\n

Сумма 100 удобна как контроль записи. Она не превращает шкалу в физическую величину. Оценка 3 не означает, что вариант в три раза лучше оценки 1. Если шкала порядковая, так и пишите. Её задача — поддержать разговор о приоритетах, а не создать научный вид.

\n

Учебный пример: остановить скрытую конфигурацию

\n

Ниже — учебный JavaScript-пример. Он не запускает технологии, не читает файлы и не получает данные из среды. Функция проверяет только структуру заранее заданного объекта. Имена вариантов условны. Код показывает отрицательный путь: если конфигурация скрыта, функция не выдаёт победителя.

\n
function 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

Это важное ограничение автоматизации. Проверка полей ловит пропуск, но не знает, подходит ли вход реальному пользователю. Она не проверяет корректность бенчмарка, статистический метод, права доступа и стоимость поддержки. Для этого нужны отдельные владельцы и доказательства.

\n

Как читать trade-off

\n

Сравнение редко даёт вариант, который лучше по всем осям. Быстрый runtime может потребовать больше обучения. Инструмент с простой миграцией может усложнить диагностику. Библиотека с хорошими метриками может ограничить формат данных. Такие отношения образуют границу компромиссов.

\n

Поэтому не спрашивайте «кто победил вообще». Спросите, какой компромисс допустим для данной операции. Если миграцию нельзя откатить за рабочее окно, стоимость обратимости получает больший вес. Если операция чувствительна к задержке, важнее зафиксировать задержку именно этой операции, а не пересказывать общий результат демо.

\n

Проверяйте чувствительность решения. Измените один вес и посмотрите, меняется ли порядок вариантов. Если небольшое изменение переворачивает вывод, решение хрупкое. Это не доказывает, что оно неверно. Это показывает, что нужно уточнить критерии и границы данных. Без реальных оценок такая проверка остаётся подготовкой, а не доказательством устойчивости.

\n

Порядок действий

\n
  1. Опишите наблюдаемый симптом: что сравнение скрывает, где это видно и сколько стоит ошибочный выбор.
  2. Сформулируйте один вопрос и назовите минимум две альтернативы. Не называйте вариант победителем до проверки условий.
  3. Разделите критерии на пригодность, стоимость внедрения и эксплуатацию либо на другие явно объяснённые группы.
  4. Зафиксируйте владельца каждого критерия и веса до получения результата. Проверьте, что сумма весов равна 100.
  5. Опишите вход, версию, окружение, операцию, повторения и правило обработки вариации.
  6. Сохраните неизвестные значения как неизвестные. Не подставляйте ноль, среднее из одного прогона или оценку из памяти.
  7. Проверьте отрицательный путь: отсутствующее поле, скрытую конфигурацию, неверную сумму и запрещённый положительный вывод.
  8. Сравните варианты по каждому критерию отдельно. Затем запишите компромисс, границу применимости и решение уполномоченного владельца.
  9. После изменения проверьте тот же сигнал на той же границе. Если условие изменилось, это новый замер, а не продолжение старого.
\n

Ограничения и отрицательный путь

\n

Этот механизм не выбирает язык, runtime или базу данных. Он не определяет допустимую нагрузку и не выдаёт статистическую значимость. Он не заменяет security review, оценку лицензий, accessibility-проверку, финансовую модель и план отката.

\n

Учебная матрица может помочь увидеть пробел, но не создаёт production-результат. Не вставляйте в неё реальные секреты, идентификаторы клиентов и ссылки на закрытые панели. Если нужно сравнить рабочие системы, сначала получите разрешение на данные и опишите протокол. Пока этого нет, допустим только структурный разбор.

\n

Отрицательный путь должен оставаться нормальным исходом. Неверный вес возвращает вопрос к критериям. Скрытый input возвращает вопрос к конфигурации. Большой разброс возвращает вопрос к нагрузке и повторениям. Наличие stop не означает провал команды. Оно показывает, что следующий вывод пока нельзя защищать.

\n

Критерий готовности

\n

Сравнение готово к решению, когда читатель может назвать операцию, вход, версии, повторения, критерии и веса. Для каждого числа есть источник и правило интерпретации. Для каждого неизвестного поля есть явная отметка и следующий способ проверки. Для каждого варианта описаны сильная сторона, цена внедрения, эксплуатационный риск и условие отката.

\n

Готовность не равна строке «выбран вариант B». Она означает, что решение можно оспорить по частям: отдельно проверить вход, отдельно пересмотреть вес, отдельно повторить измерение. Если хотя бы один слой скрыт, корректный результат — stop, а не красивый рейтинг.

\n

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

\n" + "contentHtml": "

Команда сравнивает два runtime для одной операции. В демо вариант A отвечает быстрее, а вариант B проще выглядит в коде. Через неделю появляется таблица с баллами 8,7 и 7,9, но в ней нет версии окружения, размера входа, числа повторений и стоимости перехода. Симптом узнаваем: число выглядит точным, однако его нельзя связать с конкретной нагрузкой.

\n

Цена ошибки проявится после выбора. Код окажется на неподходящей границе, обучение и сопровождение займут больше времени, а сбой объяснят «шумом измерения». Вернуться трудно, потому что исходные условия не записали. Поэтому сравнение технологии — не конкурс инструментов и не поиск вечного победителя. Это проверка конкретного решения: что нужно узнать, что действительно измерено, где остаётся неопределённость и какие предпочтения принимает владелец.

\n

Начните с вопроса, а не с названия технологии

\n

Хороший вопрос задаёт границу решения. Формулировка «какой runtime лучше» не имеет проверяемого ответа. Вопрос «какой вариант уменьшает работу по миграции для сериализации объекта размером 1 МБ, сохраняя текущий формат API и возможность отката» уже указывает операцию, вход, ограничение и ожидаемое действие.

\n

Разделите запись на четыре слоя. Вопрос говорит, какое решение предстоит принять. Наблюдение описывает результат конкретного запуска: например, длительность операции, число ошибок или расход памяти. Неопределённость показывает, какие условия могут изменить результат. Предпочтение задаёт приоритет: команда может считать обратимость важнее небольшой разницы в задержке.

\n

Эти слои нельзя подменять. Вес 40 не доказывает пригодность варианта. Балл 3 не означает, что инструмент в три раза лучше варианта с баллом 1. Фраза «надёжнее» не заменяет путь отказа и способ его воспроизвести. Если значение неизвестно, его нужно записать как unknown, а не превращать в аккуратный ноль.

\n

Зафиксируйте границу сравнения

\n

До запуска назовите одну операцию и её начало и конец. Для сериализации это может быть время от передачи подготовленного объекта функции до получения строки. Не смешивайте его с чтением файла, сетевым вызовом и записью в лог: тогда измерение отвечает уже на другой вопрос.

\n

Запишите вход в форме, которую можно получить снова: размер и структура данных, кодировка, число элементов, допустимые ошибки. Затем закрепите версию runtime, зависимости, операционную систему, процессор и параметры запуска. «Та же машина» недостаточно, если неясно, менялись ли фоновые процессы, режим энергопотребления или сборщик мусора.

\n

Отдельно опишите успех и остановку. Успехом может быть завершение операции без потери данных при заданном лимите времени. Остановкой — исключение, неверный результат, нарушение формата или отсутствие входной конфигурации. Правило остановки не даёт команде компенсировать критический дефект высоким баллом по второстепенному критерию.

\n
\"Матрица
Наблюдение, стоимость и решение отвечают на разные вопросы. Пока граница сравнения и неизвестные не названы, утверждать эффект рано.
\n

Как отличить evidence от впечатления

\n

Evidence — это не любое число в таблице, а число с происхождением и условиями получения. Минимальная запись измерения должна отвечать на пять вопросов: что измеряли, каким входом, в какой версии, сколько раз повторили и как обработали разброс. К ней полезно приложить сырые результаты или ссылку на артефакт, который другой инженер сможет открыть.

\n

Официальные руководства по измерительной неопределённости требуют описывать компоненты, влияющие на результат, а не публиковать только итог. Это не означает, что для каждой внутренней проверки нужна лабораторная методика. Это означает, что команда должна отличать случайное колебание от изменения условия и не называть единичный запуск устойчивым фактом.

\n

Документация Python timeit хорошо показывает практическую ловушку: замер может зависеть от других процессов, поэтому серия запусков полезнее одного значения, а в типичном случае нужно смотреть на весь вектор результатов, а не механически вычислять среднее. Это рекомендация конкретного инструмента, не универсальная статистическая формула. Для своей операции заранее выберите правило: диапазон, квантили, минимум или другой показатель — и объясните, почему он отвечает на вопрос.

\n

Практика воспроизводимых артефактов в ACM добавляет ещё один критерий: результаты должны быть связаны с описанными кодом, данными, версиями и инструкцией запуска. Для инженерного выбора это означает простой тест: сможет ли коллега восстановить условия без устного пояснения автора? Если нет, таблица пока фиксирует мнение, а не проверяемое сравнение.

\n

Таблица диагностики: симптом → причина → проверка → действие

\n
Проверка инженерного сравнения до выбора технологии
СимптомПричинаПроверкаДействие
Есть итоговый балл, но нет входных условийЛокальное наблюдение выдали за общий результатНайти версию, вход, нагрузку и границу операцииУбрать итог и записать недостающие условия
Веса появились после демоПредпочтение подогнали под понравившийся результатСравнить время изменения веса с моментом получения результатаВернуть веса на обсуждение до нового замера
Написано «одинаковая среда»Конфигурацию спрятали за общей фразойРаскрыть версии, зависимости, входы и лимитыОстановить сравнение до явной конфигурации
Есть среднее, но нет разбросаНеопределённость потеряли при агрегацииПроверить сырые прогоны и правило обработки вариацииПоказать диапазон или назвать значение неизвестным
Вариант назван лучшим во всех условияхКомпромисс заменили универсальным рейтингомПроверить, какие свойства ухудшаются у «победителя»Описать trade-off и границу применимости
\n

Сделайте стоимость перехода измеримой

\n

Стоимость внедрения начинается не с цены лицензии. Для каждой альтернативы перечислите обучение, изменение кода, перенос данных, интеграцию со сборкой и мониторингом, документацию, дежурство и откат. Не подставляйте часы из другого проекта: их можно использовать как гипотезу, но не как факт.

\n

Фраза «миграция простая» становится проверяемой только после уточнения объёма. Сколько модулей затронуто? Какое окно простоя допустимо? Что считается сохранёнными данными? Можно ли вернуть старую реализацию без ручного исправления записей? Если ответ неизвестен, добавьте отдельное поле и владельца следующей проверки.

\n

Полезно считать стоимость не одной суммой, а набором наблюдаемых работ. Тогда выясняется, что вариант с коротким happy path может требовать дорогой диагностики, а вариант с более длинной миграцией — дешёвого и надёжного отката. Число помогает сравнить зафиксированный объём, но не отменяет описания предположений.

\n

Вес критерия — договорённость, а не метрика

\n

Взвешенная матрица делает приоритеты видимыми. Например, команда может назначить пригодности вес 40, стоимости перехода 35, эксплуатации 25. Сумма 100 удобна как контроль записи, но не превращает шкалу в физическую величину. Вес отвечает на вопрос «что для нас важнее», а evidence — на вопрос «что мы наблюдали».

\n

Для каждого веса задайте вопрос-владелец. Пригодность: какую границу задачи обязан закрыть вариант? Стоимость: какие работы входят и что делает откат обратимым? Эксплуатация: кто заметит отказ, по какому сигналу и за какое время восстановит систему? Если на один критерий нет ответственного и способа проверки, оценка должна остановиться.

\n

Не складывайте в один score несовместимые ограничения. Если потеря данных недопустима, это veto-условие, а не минус пять баллов. Если допустимая задержка превышена, высокая оценка документации не делает вариант подходящим. Сначала отсекайте запрещённые состояния, затем сравнивайте оставшиеся компромиссы.

\n

Учебный валидатор: положительный вывод только после проверки

\n

Ниже приведён самостоятельный пример на JavaScript. Он не запускает runtime, не читает файлы и не объявляет технологию победителем. Функция проверяет структуру плана: обязательные поля, сумму весов, наличие условий измерения и отсутствие неизвестного evidence. В настоящем проекте эти поля должны заполняться из разрешённых артефактов.

\n
function 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: следующий шаг становится конкретным — описать протокол, получить разрешённые данные и сохранить исходные прогоны.

\n

Валидатор проверяет форму, но не качество самого benchmark. Он не знает, реалистичен ли вход, корректна ли функция измерения, не влияют ли фоновые процессы, соблюдены ли лицензии и можно ли безопасно обработать данные. Такие вопросы требуют отдельной проверки и владельцев. Статус ready-for-review означает только, что план можно обсуждать на следующем уровне; он не разрешает rollout.

\n

Воспроизводимость начинается до запуска

\n

Сохраните конфигурацию рядом с результатом: версию runtime и зависимостей, хеш входного набора, параметры команды, число прогревов и повторов, модель оборудования, дату запуска и сырые значения. Если часть окружения нельзя раскрыть, зафиксируйте хотя бы её категорию и причину ограничения. «Локально» — не воспроизводимое описание.

\n

Разделяйте повторяемость и переносимость. Повторяемость проверяет, получаем ли мы близкие результаты при тех же условиях. Переносимость спрашивает, сохраняется ли вывод на другой машине, версии или нагрузке. Второй вопрос требует новых измерений. Нельзя объявлять его решённым потому, что один и тот же скрипт дважды дал похожее число.

\n

После изменения технологии повторите тот же сигнал на той же границе. Если одновременно поменялись вход, версия и способ измерения, это новый эксперимент. Его можно сравнить с прежним только после явного описания отличий. Такая дисциплина защищает от удобного вывода «стало быстрее», когда изменилась сама операция.

\n

Порядок действий

\n
  1. Опишите наблюдаемый симптом и цену ошибочного выбора без названия любимого инструмента.
  2. Сформулируйте один вопрос, назовите альтернативы и задайте границу операции.
  3. Зафиксируйте вход, версию, окружение, критерий успеха и условие остановки.
  4. Разделите пригодность, стоимость перехода, эксплуатацию и отдельные veto-ограничения.
  5. Назначьте владельца каждого критерия и объясните веса до получения результата.
  6. Опишите число повторов, прогрев, сырые результаты и правило обработки вариации.
  7. Отделите неизвестное от плохого результата: не заменяйте отсутствующее evidence нулём.
  8. Проверьте отрицательный путь: исключение, неверный формат, частичный сбой, невозможность отката и скрытую конфигурацию.
  9. Сравните варианты по каждому критерию, запишите компромисс и только затем сформулируйте решение владельца.
\n

Ограничения применимости

\n

Эта методика не выбирает технологию автоматически и не создаёт данные из пустой таблицы. Она не заменяет security review, юридическую проверку лицензий, оценку доступности специалистов, accessibility-проверку, финансовую модель или план восстановления. Для критических систем одного учебного benchmark недостаточно.

\n

Не переносите результат между разными операциями. Быстрее сериализовать тестовый объект не значит дешевле обслуживать очередь. Удачный запуск на одной машине не доказывает поведение под пиковым входом. Если нагрузка, версии или требования изменились, границу нужно зафиксировать заново.

\n

Не используйте в примерах реальные секреты, идентификаторы клиентов и ссылки на закрытые панели. Если данные нельзя законно или безопасно собрать, корректный исход — stop с описанием недостающего доказательства. Отложенное решение лучше, чем рейтинг, который нельзя защитить.

\n

Критерий готовности решения

\n

Сравнение готово к решению, когда другой инженер без устного контекста может назвать операцию, вход, версии, повторы, критерии, веса и правила остановки. Для каждого числа видны источник и способ обработки. Для каждого неизвестного указаны владелец и следующий шаг. Для каждого варианта названы сильная сторона, цена внедрения, эксплуатационный риск и условие отката.

\n

Финальная проверка проста: временно уберите итоговый столбец и попросите коллегу перечислить, какие факты ещё нужны для выбора. Если разговор возвращается к входу, измерению и ограничениям, матрица помогает принимать решение. Если все защищают уже напечатанного победителя, таблица стала риторикой. Верните её к наблюдаемой проблеме.

\n

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

\n" } diff --git a/editorial/agent-rewrites/042.json b/editorial/agent-rewrites/042.json index c7b2ea9..36ce19b 100644 --- a/editorial/agent-rewrites/042.json +++ b/editorial/agent-rewrites/042.json @@ -2,6 +2,6 @@ "index": 42, "slug": "editorial-2026-11-practice-technology-evaluation", "title": "Как сравнивать технологии, когда ошибка стоит дороже прототипа", - "excerpt": "Практический способ сравнить технологии по задаче, цене перехода и эксплуатации: отделить факты от предпочтений, проверить отрицательный путь и не принять пустую ячейку за нулевой балл.", - "contentHtml": "

Команда выбирает новую технологию по удачному демо. Через несколько недель выясняется, что демо не учитывало миграцию данных, обучение, права доступа и поддержку редкого сбоя. Прототип работает, а основная система ещё не готова его принять. Люди переключаются на ручной разбор, релиз откладывается, а возврат к прежнему решению становится дороже с каждым изменением.

\n

Цена ошибки здесь не равна цене лицензии или времени на первый запуск. Она включает работу, которую не записали в сравнении: перенос состояния, изменение контрактов, настройку наблюдения, обучение дежурных и обратный переход. Ошибка становится дорогой ещё и потому, что таблица с итоговым баллом выглядит убедительно. Она скрывает, какие данные измерили, какие предположили и какие критерии добавили после просмотра результата.

\n

Тезис: сравнение технологии — это проверка решения, а не конкурс инструментов. Хорошая матрица не обещает объективного победителя. Она показывает границу задачи, цену внедрения, эксплуатационный риск, качество evidence и условия, при которых вывод перестаёт действовать.

\n

Механизм: четыре разных типа утверждений

\n

Сначала разделите четыре сущности. Вопрос описывает, что нужно узнать. Наблюдение фиксирует то, что действительно получили при заданных условиях. Неопределённость показывает, где результат может измениться. Предпочтение задаёт важность критерия для конкретного решения. Эти сущности связаны, но не заменяют друг друга.

\n

Вес 35 не доказывает, что переход дешевле. Балл 3 не доказывает, что технология подходит вашему коду. Слово «надёжная» не заменяет границу отказа и способ проверки. Если в клетке нет входных данных или метода измерения, там должно стоять unknown, а не аккуратный ноль. Ноль означает известное плохое свойство. Пустое значение означает, что команда ещё не знает, что именно проверять.

\n
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

Как оценить пригодность

\n

Начинайте не с названия инструмента, а с границы задачи. «Нужна современная платформа» не позволяет проверить результат. «Нужно обрабатывать повторную доставку сообщения с сохранением порядка внутри одного ключа» уже задаёт вопрос к контракту и к отказам. Для каждой альтернативы запишите, какие части задачи она покрывает напрямую, а какие потребуют адаптера, собственной логики или изменения модели данных.

\n

Пригодность нельзя свести к числу функций в документации. Важен путь ошибки. Если операция прерывается после записи в одно хранилище и до подтверждения в другом, кто обнаружит рассогласование? Можно ли повторить действие без дубликата? Где живёт идентификатор операции? Если технология отвечает только на happy path, её высокий балл создаёт ложное чувство готовности.

\n

Стоимость внедрения — не сноска

\n

Cost of adoption состоит из нескольких работ. Назовите их отдельно: обучение команды, изменение кода, перенос данных, интеграция с инструментами сборки, наблюдение, документация, дежурство и обратимость. Не нужно сразу превращать список в финансовую модель. Нужно сделать скрытую работу видимой и назначить владельца каждого неизвестного пункта.

\n

Особенно опасна фраза «миграция простая». У неё нет проверяемого смысла, пока не названы объём данных, допустимое окно простоя, схема отката и критерий сохранности. Учебное сравнение может отметить эти поля как unknown. Оно не имеет права подставить среднюю оценку из другого проекта: другая версия, команда или форма данных меняет стоимость перехода.

\n

Эксплуатация начинается после успешного теста

\n

Технологию будет поддерживать не автор демо, а дежурная команда. Поэтому проверяйте не только пропускную способность, но и обнаружение отказа, восстановление, диагностику и обновление. Уточните, какие метрики доступны, какие события связываются одним идентификатором и что увидит оператор при частичном сбое.

\n

Фраза «работает стабильно» не является наблюдением. Нужны условия: версия, вход, длительность, нагрузка, число повторов и правило интерпретации. Без них цифра переносится на чужой контекст без основания. Если измерение ещё не разрешено или его конфигурация не описана, корректное действие — остановить сравнение, а не придумать результат.

\n
\"Матрица
Матрица удерживает критерии и веса в одном месте. Она не содержит фактических баллов и не выбирает технологию без evidence.
\n

Симптом → причина → проверка → действие

\n
Диагностика ошибок в сравнении технологий
СимптомПричинаПроверкаДействие
У каждой альтернативы есть точный итоговый баллНеизвестные данные заменили числамиПопросить вход, метод и источник каждого баллаВернуть ячейку в unknown и остановить итог
Победитель меняется после каждого обсужденияВес критерия выбран после результатаСравнить версии матрицы и время изменения весаЗафиксировать причину веса до новых наблюдений
Демо успешно, но миграция не оцененаПроверяли happy path, а не границу переходаОписать данные, откат, простой и владельцаДобавить отдельный adoption-критерий
Оператор узнаёт об отказе от пользователяЭксплуатацию приняли за наличие метрикВоспроизвести частичный сбой и пройти alert pathПотребовать сигнал, runbook и срок реакции
«Одинаковые условия» нельзя повторитьКонфигурация скрыта в окруженииПроверить версии, входы, повторения и лимитыНе называть запуск benchmark до фиксации условий
\n

Как не спутать вес и evidence

\n

Вес отвечает на вопрос «насколько этот критерий важен для решения». Evidence отвечает на вопрос «что мы наблюдали и насколько этому можно доверять». Веса 40, 35 и 25 могут быть полезной учебной конфигурацией, если команда явно объяснила приоритеты и понимает, что это не измерение. Они не превращают три неизвестных значения в доказательство.

\n

Проведите простую проверку чувствительности только после появления разрешённых данных: измените один вес в заранее заданном диапазоне и посмотрите, меняется ли порядок альтернатив. Если результат меняется от небольшого сдвига, решение зависит от предпочтения и должно так и сообщать. Если результат не меняется, это не доказывает универсальность. Он лишь устойчив к проверенному диапазону при тех же входах.

\n

Учебный валидатор и отрицательный путь

\n
function 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.

\n

Порядок действий

\n
  1. Опишите наблюдаемую проблему и цену ошибки без названия любимой технологии.
  2. Задайте одну границу решения: данные, контракт, операцию или путь отказа.
  3. Назовите альтернативы и исключите варианты, которые не могут пройти эту границу.
  4. Разделите критерии на пригодность, стоимость перехода и эксплуатацию; добавьте только то, что влияет на решение.
  5. Запишите вес каждого критерия и причину веса до появления результата.
  6. Для каждого будущего измерения укажите входы, версию среды, метод, повторы и правило трактовки неопределённости.
  7. Отделите неизвестное от плохого результата. Не заменяйте отсутствующие данные нулём.
  8. Проверьте отрицательный путь: частичный сбой, откат, потеря наблюдения и невозможность повторить условия.
  9. Передайте матрицу на ревью с явным статусом: план, измерение или решение. Не смешивайте статусы.
\n

Когда сравнение нужно остановить

\n

Остановитесь, если критерий не имеет владельца и метода проверки. Остановитесь, если конфигурация измерения скрывает версии, входы или лимиты. Остановитесь, если веса появились после того, как стал виден удобный результат. Остановитесь, если таблица требует назвать победителя, хотя наблюдений нет. Такой stop не означает, что технология плоха. Он означает, что вопрос ещё не готов к честному ответу.

\n

Есть и отрицательный путь после внедрения. Если стоимость поддержки превысила исходное допущение или оператор не может восстановить систему в заданное время, не защищайте первоначальный выбор суммой баллов. Зафиксируйте, какое условие нарушилось, ограничьте дальнейшее распространение решения и проверьте обратимость. Матрица нужна для пересмотра, а не для оправдания уже потраченной работы.

\n

Ограничения

\n

Матрица не оценивает всё. Она не заменяет проверку безопасности, лицензий, юридических требований, доступности специалистов и совместимости с конкретной инфраструктурой. Она не создаёт данные, которых нет, и не переносит benchmark из одного окружения в другое. Она также не решает конфликт приоритетов сама: владелец решения должен объяснить, почему одна цена важнее другой.

\n

Сумма баллов может упростить разговор, но скрывает форму trade-off. Альтернатива с меньшей ценой перехода может требовать больше ручной поддержки. Альтернатива с лучшим happy path может хуже вести себя при восстановлении. Если один критический отказ недопустим, его нельзя компенсировать высокими баллами по второстепенным критериям. Задайте veto-условие отдельно.

\n

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

\n

Сравнение готово к решению, когда другой инженер может восстановить его без устного контекста: видит проблему и границу; знает альтернативы; понимает смысл каждого критерия и веса; открывает источник каждого наблюдения; видит версии, входы, повторы и ограничения; может пройти отрицательный путь; знает владельца решения и срок пересмотра. До этого документ готов только к уточнению.

\n

Практическая финальная проверка проста. Удалите итоговый столбец и попросите коллегу ответить, какие данные ещё нужны для выбора. Если ответ не меняется, матрица действительно отделяет вопрос от результата. Если коллега вынужден защищать уже напечатанного победителя, таблица стала риторическим инструментом. Верните её к наблюдаемой проблеме.

\n

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

" + "excerpt": "Практический способ сравнить технологии по границе задачи: отделить факт от допущения, посчитать цену перехода, проверить отказ и не выдавать пустую ячейку за нулевой результат.", + "contentHtml": "

Команда выбирает новую технологию по удачному демо. Через несколько недель выясняется, что демо не учитывало миграцию данных, обучение, права доступа и поддержку редкого сбоя. Прототип работает, а основная система ещё не готова его принять. Люди переключаются на ручной разбор, релиз откладывается, а возврат к прежнему решению становится дороже с каждым изменением.

\n

Цена ошибки здесь не равна цене лицензии или времени на первый запуск. Она включает работу, которую не записали в сравнении: перенос состояния, изменение контрактов, настройку наблюдения, обучение дежурных и обратный переход. Ошибка становится дорогой ещё и потому, что таблица с итоговым баллом выглядит убедительно. Она скрывает, какие данные измерили, какие предположили и какие критерии добавили после просмотра результата.

\n

Тезис: сравнение технологии — это проверка решения, а не конкурс инструментов. Хорошая матрица не обещает объективного победителя. Она показывает границу задачи, цену внедрения, эксплуатационный риск, качество evidence и условия, при которых вывод перестаёт действовать.

\n

Механизм: четыре разных типа утверждений

\n

Сначала разделите четыре сущности. Вопрос описывает, что нужно узнать. Наблюдение фиксирует то, что действительно получили при заданных условиях. Неопределённость показывает, где результат может измениться. Предпочтение задаёт важность критерия для конкретного решения. Эти сущности связаны, но не заменяют друг друга.

\n

Вес 35 не доказывает, что переход дешевле. Балл 3 не доказывает, что технология подходит вашему коду. Слово «надёжная» не заменяет границу отказа и способ проверки. Если в клетке нет входных данных или метода измерения, там должно стоять unknown, а не аккуратный ноль. Ноль означает известное плохое свойство. Пустое значение означает, что команда ещё не знает, что именно проверять.

\n
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

Как оценить пригодность

\n

Начинайте не с названия инструмента, а с границы задачи. «Нужна современная платформа» не позволяет проверить результат. «Нужно обрабатывать повторную доставку сообщения с сохранением порядка внутри одного ключа» уже задаёт вопрос к контракту и к отказам. Для каждой альтернативы запишите, какие части задачи она покрывает напрямую, а какие потребуют адаптера, собственной логики или изменения модели данных.

\n

Пригодность нельзя свести к числу функций в документации. Важен путь ошибки. Если операция прерывается после записи в одно хранилище и до подтверждения в другом, кто обнаружит рассогласование? Можно ли повторить действие без дубликата? Где живёт идентификатор операции? Если технология отвечает только на happy path, её высокий балл создаёт ложное чувство готовности.

\n

Стоимость внедрения — не сноска

\n

Cost of adoption состоит из нескольких работ. Назовите их отдельно: обучение команды, изменение кода, перенос данных, интеграция с инструментами сборки, наблюдение, документация, дежурство и обратимость. Не нужно сразу превращать список в финансовую модель. Нужно сделать скрытую работу видимой и назначить владельца каждого неизвестного пункта.

\n

Особенно опасна фраза «миграция простая». У неё нет проверяемого смысла, пока не названы объём данных, допустимое окно простоя, схема отката и критерий сохранности. Учебное сравнение может отметить эти поля как unknown. Оно не имеет права подставить среднюю оценку из другого проекта: другая версия, команда или форма данных меняет стоимость перехода.

\n

Эксплуатация начинается после успешного теста

\n

Технологию будет поддерживать не автор демо, а дежурная команда. Поэтому проверяйте не только пропускную способность, но и обнаружение отказа, восстановление, диагностику и обновление. Уточните, какие метрики доступны, какие события связываются одним идентификатором и что увидит оператор при частичном сбое.

\n

Фраза «работает стабильно» не является наблюдением. Нужны условия: версия, вход, длительность, нагрузка, число повторов и правило интерпретации. Без них цифра переносится на чужой контекст без основания. Если измерение ещё не разрешено или его конфигурация не описана, корректное действие — остановить сравнение, а не придумать результат.

\n
\"Матрица
Матрица удерживает критерии и веса в одном месте. Она не содержит фактических баллов и не выбирает технологию без evidence.
\n

Симптом → причина → проверка → действие

\n
Диагностика ошибок в сравнении технологий
СимптомПричинаПроверкаДействие
У каждой альтернативы есть точный итоговый баллНеизвестные данные заменили числамиПопросить вход, метод и источник каждого баллаВернуть ячейку в unknown и остановить итог
Победитель меняется после каждого обсужденияВес критерия выбран после результатаСравнить версии матрицы и время изменения весаЗафиксировать причину веса до новых наблюдений
Демо успешно, но миграция не оцененаПроверяли happy path, а не границу переходаОписать данные, откат, простой и владельцаДобавить отдельный adoption-критерий
Оператор узнаёт об отказе от пользователяЭксплуатацию приняли за наличие метрикВоспроизвести частичный сбой и пройти alert pathПотребовать сигнал, runbook и срок реакции
«Одинаковые условия» нельзя повторитьКонфигурация скрыта в окруженииПроверить версии, входы, повторения и лимитыНе называть запуск benchmark до фиксации условий
\n

Как не спутать вес и evidence

\n

Вес отвечает на вопрос «насколько этот критерий важен для решения». Evidence отвечает на вопрос «что мы наблюдали и насколько этому можно доверять». Веса 40, 35 и 25 могут быть полезной учебной конфигурацией, если команда явно объяснила приоритеты и понимает, что это не измерение. Они не превращают три неизвестных значения в доказательство.

\n

Проведите простую проверку чувствительности только после появления разрешённых данных: измените один вес в заранее заданном диапазоне и посмотрите, меняется ли порядок альтернатив. Если результат меняется от небольшого сдвига, решение зависит от предпочтения и должно так и сообщать. Если результат не меняется, это не доказывает универсальность. Он лишь устойчив к проверенному диапазону при тех же входах.

\n

Учебный валидатор и отрицательный путь

\n
function 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.

\n

Порядок действий

\n
  1. Опишите наблюдаемую проблему и цену ошибки без названия любимой технологии.
  2. Задайте одну границу решения: данные, контракт, операцию или путь отказа.
  3. Назовите альтернативы и исключите варианты, которые не могут пройти эту границу.
  4. Разделите критерии на пригодность, стоимость перехода и эксплуатацию; добавьте только то, что влияет на решение.
  5. Запишите вес каждого критерия и причину веса до появления результата.
  6. Для каждого будущего измерения укажите входы, версию среды, метод, повторы и правило трактовки неопределённости.
  7. Отделите неизвестное от плохого результата. Не заменяйте отсутствующие данные нулём.
  8. Проверьте отрицательный путь: частичный сбой, откат, потеря наблюдения и невозможность повторить условия.
  9. Передайте матрицу на ревью с явным статусом: план, измерение или решение. Не смешивайте статусы.
\n

Когда сравнение нужно остановить

\n

Остановитесь, если критерий не имеет владельца и метода проверки. Остановитесь, если конфигурация измерения скрывает версии, входы или лимиты. Остановитесь, если веса появились после того, как стал виден удобный результат. Остановитесь, если таблица требует назвать победителя, хотя наблюдений нет. Такой stop не означает, что технология плоха. Он означает, что вопрос ещё не готов к честному ответу.

\n

Есть и отрицательный путь после внедрения. Если стоимость поддержки превысила исходное допущение или оператор не может восстановить систему в заданное время, не защищайте первоначальный выбор суммой баллов. Зафиксируйте, какое условие нарушилось, ограничьте дальнейшее распространение решения и проверьте обратимость. Матрица нужна для пересмотра, а не для оправдания уже потраченной работы.

\n

Ограничения

\n

Матрица не оценивает всё. Она не заменяет проверку безопасности, лицензий, юридических требований, доступности специалистов и совместимости с конкретной инфраструктурой. Она не создаёт данные, которых нет, и не переносит benchmark из одного окружения в другое. Она также не решает конфликт приоритетов сама: владелец решения должен объяснить, почему одна цена важнее другой.

\n

Сумма баллов может упростить разговор, но скрывает форму trade-off. Альтернатива с меньшей ценой перехода может требовать больше ручной поддержки. Альтернатива с лучшим happy path может хуже вести себя при восстановлении. Если один критический отказ недопустим, его нельзя компенсировать высокими баллами по второстепенным критериям. Задайте veto-условие отдельно.

\n

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

\n

Сравнение готово к решению, когда другой инженер может восстановить его без устного контекста: видит проблему и границу; знает альтернативы; понимает смысл каждого критерия и веса; открывает источник каждого наблюдения; видит версии, входы, повторы и ограничения; может пройти отрицательный путь; знает владельца решения и срок пересмотра. До этого документ готов только к уточнению.

\n

Практическая финальная проверка проста. Удалите итоговый столбец и попросите коллегу ответить, какие данные ещё нужны для выбора. Если ответ не меняется, матрица действительно отделяет вопрос от результата. Если коллега вынужден защищать уже напечатанного победителя, таблица стала риторическим инструментом. Верните её к наблюдаемой проблеме.

\n

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

" } diff --git a/editorial/agent-rewrites/043.json b/editorial/agent-rewrites/043.json index 0f8c8f3..43285e4 100644 --- a/editorial/agent-rewrites/043.json +++ b/editorial/agent-rewrites/043.json @@ -2,6 +2,6 @@ "index": 43, "slug": "editorial-2026-10-field-code-review-standard", "title": "Code review: как не пропустить риск изменения контракта", - "excerpt": "Форматирование занимает строки в комментариях, а необратимое изменение контракта остаётся без проверки. Разбираем порядок review, который связывает симптом, evidence, отрицательный путь и решение.", - "contentHtml": "

В pull request меняют поле ответа с обязательного на nullable. В комментариях спорят о названии функции, порядке импортов и длине строки. Через неделю старый клиент падает на пустом значении. Ошибка возникла не в синтаксисе. Review проверил видимый diff, но не проверил границу контракта. Цена такого пропуска — аварийный откат, срочный выпуск совместимости и потеря времени у команды, которая теперь ищет всех потребителей вслепую.

\n

Тезис простой: code review должен связывать каждый существенный риск с проверяемым evidence. Если изменение меняет форму данных, одного чтения строк недостаточно. Нужно назвать потребителей, переходы состояния и путь возврата. Если evidence не хватает, reviewer формулирует точный вопрос и останавливает сильный вывод. Он не заменяет пробел догадкой и не маскирует его стилевым комментарием.

\n

Сначала отделите симптом от причины

\n

Симптом обычно виден в обсуждении: много мелких замечаний, спор о вкусе, длинный список предложений без одного вопроса о поведении системы. Это не доказывает плохой review. Но это сигнал проверить, не вытеснил ли стиль риск. Причина часто лежит за пределами изменённого файла: у поля есть другой consumer, миграция не обратима, а тест покрывает только новый путь.

\n

Начните с вопроса: что изменится для пользователя или соседнего сервиса, если этот diff попадёт в основную ветку? Ответ должен быть конкретным. «Станет современнее» не подходит. «Клиент, который не различает null и отсутствие поля, получит другой результат» — подходит. Следующий вопрос: каким артефактом это можно проверить? Это может быть schema delta, карта потребителей, тест на старую форму или явная инструкция отката. Список должен быть конечным.

\n

Механизм evidence map

\n

Удобно хранить review как короткую связку из пяти полей: change, risk, evidence, status и next action. Change называет один предмет. Risk описывает тип последствий, а не эмоциональную оценку. Evidence перечисляет входы, которыми можно проверить риск. Status показывает границу текущего вывода. Next action говорит, что должен сделать следующий владелец.

\n
change: 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

Пример: nullable-поле и скрытый consumer

\n

Представьте учебный API ответа со скидкой. Было discount: number, стало discount: number | null. Сервер может собрать ответ, а новый тест может пройти. Но старый клиент способен сразу передать значение в арифметику или отрисовать его без ветки для null. Поэтому строка изменения ещё не является достаточным evidence.

\n
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

Симптом → причина → проверка → действие

\n
Как переводить наблюдение в следующий проверяемый шаг
СимптомПричинаПроверкаДействие
Комментарии заполнены форматированиемРиск поведения не названСверить diff с целью и контрактомСнять style-only комментарии и задать один вопрос о последствиях
Поле стало nullableНе видны все consumersПроверить schema delta и карту потребителейЗапросить конкретный список клиентов и обработку null
Есть слово rollbackНе описано, что возвращаетсяСопоставить старую форму и переход состоянияПопросить шаг возврата и условие его применимости
Тест проходит только на новом ответеОтрицательный путь отсутствуетПодать старую форму и nullДобавить проверку отказа или безопасного значения
Автор просит approve при неполном inputВывод сильнее evidenceПроверить обязательные поля risk-классаОстановить review с перечнем недостающих данных
\n

Как выглядит отрицательный путь

\n

Надёжный стандарт должен объяснять остановку так же ясно, как положительный путь. Если отсутствует consumer map, статус — «недостаточно evidence», а действие — запросить только карту. Не нужно добавлять «вероятно безопасно» или искать потребителей по памяти. Если reviewer видит только изменение стиля, а риск относится к контракту, стилевой комментарий не закрывает проверку. Если risk class неизвестен, сначала нужно назвать его границы.

\n

Есть и другой стоп-сигнал: все обязательные артефакты перечислены, но итоговая фраза говорит «approve and merge». Полный набор входов не превращает учебную карточку в разрешение на слияние. В настоящем процессе approval зависит от полномочий, политики репозитория и результата остальных проверок. В записи review лучше разделять «evidence достаточно для следующего вопроса» и «изменение готово к merge».

\n
function 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-автоматизации те же статусы потребуют отдельного контракта, тестов и владельца.

\n
\"Петля
Проверка должна замыкаться на evidence: неполный вход возвращает точный вопрос, а не уверенный вердикт.
\n

Порядок проверки

\n
  1. Назовите цель изменения одним предложением. Укажите, какая форма данных, граница доступа или переход состояния меняется.
  2. Выберите один риск-класс. Для nullable-поля это совместимость контракта, а не абстрактное «качество кода».
  3. Составьте короткий список обязательного evidence: schema delta, потребители, отрицательный путь и способ возврата, если он нужен.
  4. Проверьте каждый пункт по конкретному артефакту. Если ссылки нет или артефакт не отвечает на вопрос, пометьте пункт как отсутствующий.
  5. Сначала прогоните отрицательные ветки: нет карты потребителей, выбран только стиль, не назван risk class, вывод просит approval.
  6. Сформулируйте действие с одним владельцем и одним недостающим входом. Не отправляйте список предположений.
  7. После получения evidence повторите проверку границы. Убедитесь, что вывод не стал сильнее данных и что новый тест покрывает отказной путь.
\n

Ограничения

\n

Evidence map не заменяет архитектурное решение, security assessment или эксплуатационную проверку. Он не вычисляет severity, не назначает SLA и не доказывает отсутствие дефекта. Для миграции данных понадобятся отдельные вопросы о совместимости версий, объёме записей и восстановлении. Для security-риска понадобятся trust boundary, правило входа и наблюдаемый сценарий злоупотребления. Нельзя переносить набор полей из одного риска в другой без проверки.

\n

Стандарт также не делает review быстрым автоматически. Иногда карта потребителей дороже самого изменения. Это нормальная цена, если поле пересекает границу сервиса. Если изменение локально и контракт не меняется, достаточно меньшего набора evidence. Смысл стандарта не в максимальном числе проверок, а в соразмерности: риск определяет обязательные входы.

\n

Не следует превращать каждое замечание в блокирующее. Комментарий о названии может улучшить читаемость, но не должен изображать угрозу совместимости. И наоборот, отсутствие доказательства по контракту нельзя закрывать фразой «потом посмотрим». Разделяйте обязательное условие и полезное предложение.

\n

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

\n

Review готов для передачи решения, когда выполнены четыре условия: цель изменения понятна; риск назван; каждый обязательный вход имеет проверяемый источник; отрицательный путь возвращает явное действие. Дополнительно проверьте, что итоговая формулировка соответствует данным. Если карта потребителей не полна, критерий не выполнен. Если evidence полон, это ещё не равно approval: это означает, что вопрос можно передать владельцу контракта с понятной границей.

\n

Практический тест можно выполнить на учебном объекте. Удалите consumer map — запись должна вернуть stop-insufficient-evidence. Замените проверку риска на style-only — запись должна вернуть stop-style-displaces-risk. Добавьте недопустимое слово approval — запись должна остановиться. Верните все поля и оставьте вывод ограниченным вопросом — запись должна пройти как готовая evidence map. Эти результаты проверяют механику примера, а не production-поведение.

\n

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

" + "excerpt": "Форматирование занимает строки в комментариях, а изменение контракта остаётся без проверки. Разбираем порядок review, который связывает симптом, evidence, отрицательный путь и решение.", + "contentHtml": "

В pull request поле ответа меняют с обязательного на nullable. В обсуждении появляются замечания о названии функции, порядке импортов и длине строки. Через неделю старый клиент получает null и падает в арифметике. Проблема возникла не в синтаксисе: review проверил видимый diff, но не проверил границу контракта. Цена ошибки — откат, срочный выпуск совместимости и поиск всех потребителей в условиях сбоя.

\n

Code review должен отвечать не только на вопрос «понятно ли написан код», но и на вопрос «что изменилось для каждого участника контракта». Для этого reviewer связывает изменение с одним классом риска, проверяемым evidence и разрешённым выводом. Если evidence неполно, сильный вывод нужно остановить. «Выглядит безопасно» не заменяет список потребителей, тест отрицательного пути или описание возврата.

\n

Начните с границы изменения

\n

Сначала опишите старое и новое поведение одним предложением. Например: «Ответ Price теперь допускает discount: null, а клиент должен отличать отсутствие скидки от ошибки». В такой формулировке видны данные, потребитель и новая ветка. Фраза «улучшили модель» для review слишком широка: по ней нельзя выбрать проверку.

\n

Затем назовите границу, которую пересекает diff. Для API это producer, транспорт, schema и consumer. Для фоновой задачи — состояние до операции, событие, состояние после него и эффект повтора. Для входных данных — источник, правило валидации, trust boundary и последствие нарушения. Один и тот же файл может затронуть несколько границ, но для первого вопроса выберите ту, где цена ошибки выше.

\n

Такой порядок согласуется с практикой code review, где сначала выясняют назначение изменения, затем смотрят design и functionality, а после — tests, edge cases и контекст. Проверка строк без понимания границы легко превращается в перечень предпочтений. Проверка границы даёт обозримый вопрос: какой потребитель увидит новую форму, какое состояние повторится или какой вход пересечёт доверенную зону.

\n

Evidence должно отвечать на конкретный вопрос

\n

Evidence — не любое вложение в pull request, а артефакт, который отвечает на названный вопрос. Для изменения контракта это обычно schema delta, карта категорий потребителей, примеры старого и нового ответа, тесты совместимости и описание обратного перехода. Каждый пункт должен иметь владельца и понятный результат. Слова «тесты зелёные» недостаточно: нужно указать, какое свойство тест проверяет и на каком входе.

\n
Минимальная карта review для изменения контракта
ВопросEvidenceЧто оно подтверждаетЧего не подтверждает
Что изменилось?Старая и новая schemaФорму, обязательность и допустимые значенияПоведение каждого клиента
Кто читает ответ?Карта consumer-категорий и места декодированияГраницу поиска потребителейСовместимость без теста или чтения кода
Что будет при старой форме?Compatibility test с v1 writer и v2 readerРезультат конкретной пары версийВсе комбинации rollout
Что будет при новой форме?Тест старого reader на новом ответеПоведение выбранного старого потребителяПотребителей, которых не включили в выборку
Как вернуться?Описание старой формы, порядка и условия откатаВозможный путь возвратаСкорость и успех отката в аварии
\n

У карты есть полезное свойство: она ограничивает вывод. Schema delta не доказывает, что миграция безопасна. Карта потребителей не доказывает, что каждый потребитель обновлён. Тест одной пары версий не доказывает поведение мобильного приложения, очереди и фонового job одновременно. Reviewer обязан держать эти границы видимыми, иначе список артефактов создаёт ложную уверенность.

\n
\"Диаграмма
Соседние границы нужно проверять последовательно: намерение не доказывает форму запроса, ответ не доказывает корректность входа в UI.
\n

Учебный кейс: nullable-поле

\n

Рассмотрим ответ магазина. В версии v1 скидка всегда была числом. В версии v2 сервер хочет сообщать, что скидка не рассчитана, через null. Это не просто изменение типа. Для клиента нужно определить смысл трёх состояний: поле отсутствует, поле равно null и поле содержит число. Если команда не различает эти состояния, новый ответ может сломать старую логику даже при валидном JSON.

\n
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, а не ограничивайтесь примером нового кода.

\n

Есть важная асимметрия rollout. Новый reader может научиться принимать старый ответ без поля, но старый reader может не уметь принимать новый null. Поэтому совместимость нужно проверять в обе стороны. Если порядок выпуска допускает встречу новых writers со старыми readers, нужен либо tolerant reader, либо временная форма ответа, либо явный запрет такого порядка. Само слово «nullable» решение не выбирает.

\n

Отделяйте style-комментарий от блокирующего риска

\n

Замечание о стиле может быть полезным, если правило закреплено в style guide или если оно мешает прочитать код. Но style-комментарий не закрывает вопрос о контракте. Google Engineering Practices прямо разделяет технические факты и личные предпочтения, а необязательное улучшение предлагает помечать как nit. В рабочем review это означает два независимых комментария: короткий style-nit и отдельный вопрос о совместимости.

\n

Блокирующий комментарий должен содержать наблюдение, риск, evidence и действие. «Похоже, сломается» — гипотеза. «Поле стало nullable, а в formatReceipt значение передаётся в арифметику без ветки; нужен тест на null или подтверждение иной границы» — проверяемый вопрос. Такой комментарий не обвиняет автора и не требует «проверить всё». Он называет один недостающий факт и ожидаемый результат.

\n
Как превратить наблюдение в рабочий комментарий
НаблюдениеСлабый выводПроверяемый комментарий
Поле стало nullable«API теперь опасный»«Покажите старых readers и их ветку для null; без этого не видна совместимость»
Есть retry после timeout«Повтор безопасен»«Какой state записан до повтора и почему побочный эффект не создаст дубль?»
Добавили проверку входа«Уязвимость закрыта»«Какой источник доверенный, какое правило проверяется и что происходит при отказе?»
Тест проходит«Можно merge»«Какой отрицательный input должен уронить тест и почему он включён?»
\n

Проверяйте отрицательный путь

\n

Положительный тест подтверждает один разрешённый вход. Риск часто скрывается в том, что происходит при отказе, повторе или старой версии. Для контрактного изменения отрицательный путь — это старый consumer, отсутствующее поле, неожиданный тип, null или невозможность вернуть прежнюю форму. Для операции — timeout после побочного эффекта и повтор запроса. Для security — недоверенный источник и вход, который проходит поверхностную проверку.

\n

Если обязательное evidence отсутствует, review должно вернуть stop, а не приблизительный approve. Stop — не оценка автора. Это состояние данных: «карта потребителей отсутствует», «неизвестна семантика null» или «не названо условие отката». После появления evidence reviewer повторяет только связанную ветку и не расширяет вывод автоматически.

\n
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, то есть можно задать узкий вопрос владельцу контракта. Это ещё не утверждение, что ответ безопасен.

\n

Три класса риска, три набора evidence

\n

Не превращайте checklist в одинаковый пакет для каждого diff. Риск выбирает доказательство, а стоимость проверки должна быть соразмерна последствиям.

\n\n

NIST SSDF предлагает рассматривать безопасную разработку как набор практик, которые встраиваются в существующий жизненный цикл, но не предписывает один инструмент или одинаковую реализацию. Документ отдельно подчёркивает зависимость от риска, стоимости, осуществимости и применимости. Поэтому security-вопрос в review нужно передавать компетентному владельцу, если граница выходит за знания reviewer.

\n

Порядок review, который можно повторить

\n
  1. Сформулируйте симптом и цену пропуска: кто увидит неверное поведение, какие данные потеряются и какой откат потребуется.
  2. Запишите старую и новую форму или состояние. Не называйте изменение «рефакторингом», если оно меняет внешний контракт.
  3. Выберите один основной класс риска и выпишите минимальное evidence для него.
  4. Проверьте intent, design и functionality, затем места потребления, тесты, edge cases и документацию, которую затрагивает изменение.
  5. Сначала прогоните отрицательную ветку: старый consumer, отказ, timeout, недоверенный input или невозможность возврата.
  6. Каждый вывод привяжите к артефакту. Если доказательство отсутствует, верните stop с точным именем missing evidence.
  7. Разделите обязательное исправление и style-nit. Не блокируйте изменение личным предпочтением и не закрывайте риск форматированием.
  8. Передайте оставшийся вопрос владельцу границы: contract owner, автору state machine, security reviewer или другой явно названной роли.
  9. После ответа проверьте, что вывод не стал сильнее evidence и что новый тест действительно падает на сломанном поведении.
\n

Ограничения применимости

\n

Этот порядок не заменяет архитектурное решение, security assessment, нагрузочный тест, миграционный план или правила защищённой ветки. Он не вычисляет severity и не гарантирует, что неизвестный consumer не существует. Карта потребителей имеет границу поиска; её нужно расширять, если меняются репозитории, версии клиентов, очереди или внешние интеграции.

\n

Учебный код намеренно мал. Он не моделирует распределённую транзакцию, реальный schema registry, авторизацию, конкурентную запись или rollout нескольких приложений. Для финансового действия добавьте идемпотентность и аудит. Для персональных данных — права доступа, минимизацию и срок хранения. Для публичного API — версию, период совместимости и коммуникацию потребителей.

\n

Не каждый diff заслуживает полного пакета. Локальное изменение имени без изменения поведения может пройти через style guide и узкий тест. Но если меняется обязательность поля, порядок побочных эффектов или trust boundary, сокращать evidence до «локально компилируется» нельзя. Состав проверки определяет последствия, а не размер diff.

\n

Критерий готовности

\n

Review готово к передаче решения, когда без догадок видны четыре вещи: симптом и цена ошибки, затронутая граница, evidence для выбранного риска и отрицательный путь. Для каждого незакрытого пункта указан один владелец и одно действие. Формулировка «можно сливать» допустима только в пределах полномочий и правил репозитория; сама evidence map этого разрешения не выдаёт.

\n

Перед отправкой итогового комментария задайте себе контрольный вопрос: «Что именно станет наблюдаемым, если моя гипотеза неверна?» Если ответа нет, это ещё не evidence. Если ответ есть, добавьте его в тест, лог, schema или карту потребителей и ограничьте вывод тем, что этот артефакт действительно показывает.

\n

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

" } diff --git a/editorial/agent-rewrites/044.json b/editorial/agent-rewrites/044.json index dd13d5f..81a04fb 100644 --- a/editorial/agent-rewrites/044.json +++ b/editorial/agent-rewrites/044.json @@ -3,5 +3,5 @@ "slug": "editorial-2026-10-mechanism-code-review-standard", "title": "Code review как механизм управления риском: evidence, stop и решение", "excerpt": "Как отличить замечание о стиле от риска контракта, состояния или границы доверия, запросить проверяемое evidence и не выдать предположение за готовое решение.", - "contentHtml": "

В pull request меняется nullable-поле. Reviewer оставляет шесть комментариев о названиях и форматировании. Никто не спрашивает, какие клиенты читают новое значение, что получит старый клиент и как вернуть прежнюю форму. После слияния один потребитель начинает трактовать null как «скидки нет», а другой — как ошибку. Ошибка появляется не в строке с новым типом. Она появляется в границе контракта.

\n

Цена такого пропуска выше цены неудачного комментария. Команда тратит время на разбор несовместимого ответа, откатывает часть изменений и выясняет, кто владеет обратимостью. При этом review могло выглядеть аккуратно. Проблема не в том, что reviewer не заметил все дефекты. Проблема в том, что вывод оказался сильнее доступных фактов.

\n

Тезис: сначала ограничьте вывод

\n

Надёжное review связывает четыре элемента: наблюдаемый симптом, класс риска, нужное evidence и допустимое действие. Evidence — это проверяемое основание вывода: форма данных, список потребителей, переход состояния или правило входа. Если основание неполно, review останавливает вывод и называет недостающий факт. Такой режим называют fail-closed: пробел не превращается в «скорее всего безопасно».

\n

Эта схема не оценивает reviewer и не делает из checklist универсальную policy. Она помогает выбрать следующий вопрос. Изменение формы ответа требует проверить контракт. Новая ветка ошибки требует проверить состояние до и после неё. Проверка входа требует определить границу доверия и возможное злоупотребление. Один комментарий о стиле не закрывает ни одну из этих границ.

\n

Механизм: риск выбирает доказательство

\n

Contract risk возникает, когда меняется форма данных или ожидание потребителя. Назовите старую и новую форму. Затем перечислите категории потребителей. После этого опишите возврат к старой форме или честно укажите, что возврат невозможен.

\n

Operational risk возникает, когда меняется поведение во времени. Запишите состояние до ветки, событие, состояние после неё и результат повтора. Слова «retry» и «timeout» сами по себе ничего не доказывают. Нужно показать, повторяется ли побочный эффект и кто увидит отказ.

\n

Security risk возникает на границе доверия. Назовите доверенную сторону, правило входа и последствие нарушения. Непривычный diff ещё не означает уязвимость. Но отсутствие границы доверия не позволяет утверждать, что проверка входа защищает систему.

\n
\"Три
Схема показывает порядок вывода: определить риск, собрать нужные факты, остановиться при пробеле и только затем передать точный вопрос владельцу границы.
\n

Gate не обязан выдавать approve или reject. Его задача уже выполнена, если он не дал неполному input породить ложное решение. При полном наборе фактов reviewer всё ещё не доказывает работоспособность всей системы. Он получает право сформулировать узкий вопрос: например, «проверьте совместимость этих потребителей с новой формой».

\n

Симптомы и действия

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
В обсуждении много style-комментариев, но нет вопроса о данныхРиск границы не названСравнить старую и новую форму, найти потребителейОстановить вывод и запросить contract evidence
Есть обработчик ошибки, но непонятно, что будет при повтореНе описан переход состоянияЗаписать state до ветки, событие, state после и эффект повтораСформулировать operational question
Валидатор принимает вход, но доверие к источнику не определеноСмешаны проверка значения и security boundaryНазвать trust boundary, input rule и abuse consequenceПередать вопрос владельцу безопасности
Комментарий говорит «безопасно» после одного тестаВывод шире evidenceСверить утверждение с тем, что реально проверил тестЗаменить вердикт на ограниченный результат
\n

Учебный пример: остановка при неполной карте потребителей

\n

Ниже показана учебная ветка для изменения контракта. Она не читает pull request, репозиторий, CI, сеть или production. Функция получает обычный объект и возвращает статус. Такой пример объясняет механизм stop, но не проверяет совместимость реальных клиентов.

\n
const 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. Он лишь разрешает задать владельцу контракта конкретный вопрос.

\n

В реальном review названия полей должны описывать факты проекта. schemaDelta — это не слово «изменился API», а точная старая и новая форма. consumerMap — не список случайных сервисов, а граница поиска и категории потребителей. rollbackNote — не обещание отката, а описание старой формы, порядка возврата и условий, при которых возврат возможен.

\n

Почему стиль часто вытесняет риск

\n

Стиль легко обсуждать: строка видна, правило знакомо, исправление локально. Контракт требует контекста. Поэтому style-комментарий не плох сам по себе. Ошибка возникает, когда он закрывает обсуждение изменения поведения. Разделяйте уровни: обязательное правило форматирования исправьте локально, а риск контракта вынесите отдельным блоком с доказательством и владельцем.

\n

То же относится к тесту. Наличие теста не равно проверке нужного свойства. Unit-тест может подтвердить, что функция вернула null. Он не показывает, как это значение трактуют старые потребители. Тест перехода состояния может подтвердить ветку ошибки. Он не доказывает, что повтор не создаёт дубль, если побочный эффект выполняется до записи статуса.

\n

Stop и escalation — разные действия

\n

Stop означает, что данных недостаточно даже для точного следующего вопроса. Если неизвестен класс риска, сначала опишите границу изменения. Если для contract risk нет карты потребителей, запросите только её. Не добавляйте «проверьте всё» — такой запрос нельзя проверить и нельзя завершить.

\n

Escalation означает, что риск и нужное evidence названы, но решение принадлежит другой роли. Вопрос о форме ответа передают владельцу контракта. Вопрос о повторе побочного эффекта — владельцу состояния или эксплуатации. Вопрос о границе доверия — владельцу безопасности. Передача должна содержать риск, факты, точный вопрос и ограничение вывода. Она не должна приписывать получателю готовый диагноз.

\n

Слабый вывод здесь полезнее громкого. «Нужно проверить совместимость потребителей с новой nullable-формой» честнее, чем «все клиенты совместимы». «Нужно уточнить повтор операции после timeout» честнее, чем «retry безопасен». «Нужно привлечь владельца trust boundary» честнее, чем «уязвимость найдена».

\n

Порядок действий

\n
  1. Опишите наблюдаемый симптом: что изменилось в diff и какое поведение может увидеть пользователь, потребитель или оператор.
  2. Назовите одну границу риска: контракт, переход состояния или доверие к входу.
  3. Сопоставьте границе минимальное evidence и отделите факт от предположения.
  4. Проверьте каждую позицию по исходному коду, тесту, схеме или документу; не заменяйте её общим «выглядит нормально».
  5. Если позиция отсутствует, верните stop с точным именем missing evidence.
  6. Если набор полон, сформулируйте ограниченный вопрос и передайте его владельцу границы.
  7. Отдельно проверьте отрицательный путь: повтор, отказ, старый потребитель, недопустимый вход или невозможность возврата.
  8. Закройте review только после проверки того свойства, ради которого меняли код; style-заметки не выдавайте за доказательство поведения.
\n

Ограничения

\n

Эта модель не считает severity, не назначает SLA и не заменяет security-аудит, контрактные тесты, нагрузочные проверки или миграционный план. Она не доказывает отсутствие дефекта. Она ограничивает силу комментария доступными фактами.

\n

Одна матрица не покрывает весь домен. Для финансовой операции могут потребоваться идемпотентность и аудит. Для публичного API — версия и период совместимости. Для персональных данных — срок хранения и права доступа. Добавляйте такие строки, когда они принадлежат конкретной границе. Не превращайте review в ритуал, где каждый change получает одинаковый пакет документов.

\n

Учебный код выше намеренно мал. Он не моделирует распределённую транзакцию, не запускает миграцию и не возвращает production-метрики. Его проверяемое свойство уже: при отсутствии consumerMap функция возвращает stop и не объявляет совместимость доказанной.

\n

Критерий готовности

\n

Review готово, когда читатель может ответить на четыре вопроса без догадок: какой симптом привёл к проверке, какая граница риска затронута, какое evidence подтверждает следующий вопрос и какое утверждение запрещено делать. Для неполного набора виден точный stop. Для полного набора есть владелец вопроса и отрицательный путь. Если вместо этих ответов остаются «выглядит хорошо», «тесты зелёные» или «потом проверим», механизм ещё не сработал.

\n

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

" + "contentHtml": "

В pull request меняется nullable-поле. Reviewer оставляет шесть комментариев о названиях и форматировании. Никто не спрашивает, какие клиенты читают новое значение, что получит старый клиент и как вернуть прежнюю форму. После слияния один потребитель начинает трактовать null как «скидки нет», а другой — как ошибку. Ошибка появляется не в строке с новым типом, а на границе контракта.

\n

Цена такого пропуска измеряется не количеством комментариев, а временем восстановления: команда разбирает несовместимый ответ, откатывает часть изменений и ищет владельца обратимости. При этом review выглядит аккуратно. Значит, проблема не в недостатке стилистических замечаний, а в том, что итоговый вывод оказался сильнее доступных фактов.

\n

Вопрос review: что именно мы уже знаем

\n

Надёжное review связывает четыре элемента: наблюдаемый симптом, класс риска, нужное evidence и допустимое действие. Evidence — проверяемое основание вывода: форма данных, список потребителей, переход состояния или правило входа. Если основание неполно, review останавливает вывод и называет недостающий факт. Это рабочее правило fail-closed: пробел не превращается в «скорее всего безопасно».

\n

Такой подход согласуется с двумя официальными ориентирами. GitHub описывает review как просмотр commits, изменённых файлов и diff перед решением approve или request changes. Руководство Google ставит выше личных предпочтений технические факты и данные, а целью review называет улучшение общего состояния кодовой базы. Эти документы не задают одну policy для всех команд, но дают проверяемую границу: комментарий должен помогать оценить изменение, а не только выражать вкус.

\n

Класс риска определяет evidence

\n

Contract risk появляется, когда меняется форма данных или ожидание потребителя. Зафиксируйте старую и новую форму, перечислите категории потребителей и укажите, как вернуть прежний ответ. Если обратимость невозможна, это должно быть частью решения, а не обещанием в комментарии.

\n

Operational risk появляется, когда меняется поведение во времени. Запишите состояние до ветки, событие, состояние после неё и результат повтора. Слова retry и timeout ничего не доказывают сами по себе: нужно показать, повторяется ли побочный эффект и кто увидит отказ.

\n

Security risk появляется на границе доверия. Назовите доверенную сторону, правило входа и последствие нарушения. Непривычный diff ещё не означает уязвимость. Но отсутствие карты доверия не позволяет утверждать, что проверка входа защищает систему.

\n
\"Три
Порядок вывода: определить границу риска, собрать соответствующие факты, остановиться при пробеле и только затем передать точный вопрос владельцу.
\n

Gate не обязан выдавать approve или reject. Его задача уже выполнена, если неполный input не породил ложное решение. При полном наборе фактов reviewer всё ещё не доказывает работоспособность всей системы. Он получает право сформулировать узкий вопрос: «проверьте совместимость этих потребителей с новой формой».

\n

Симптомы, проверка и действие

\n
Как превратить наблюдаемый симптом в ограниченное действие
СимптомГипотеза о рискеМинимальная проверкаДействие
В обсуждении много style-комментариев, но нет вопроса о данныхНе названа граница контрактаСравнить старую и новую форму, найти категории потребителейОстановить вывод и запросить карту совместимости
Есть обработчик ошибки, но непонятно, что будет при повтореНе описан переход состоянияЗаписать состояние до события, после события и эффект повтораЗадать operational-вопрос владельцу состояния
Валидатор принимает вход, но источник доверия не определёнСмешаны проверка значения и security boundaryНазвать trust boundary, правило входа и последствие обходаПередать точный вопрос владельцу безопасности
Комментарий говорит «безопасно» после одного тестаВывод шире проверенного свойстваСопоставить утверждение с входами, ветками и потребителями тестаЗаменить вердикт на ограниченный результат
\n

У таблицы есть практическая граница: она не ранжирует severity и не определяет владельца автоматически. Её задача — не потерять первый диагностический шаг. Если в одной строке одновременно появляются три разных риска, разделите их: иначе evidence станет слишком общим и stop снова превратится в «проверьте всё».

\n

Воспроизводимый пример stop

\n

Ниже — самостоятельная функция для проверки полноты входной карты. Она не читает pull request, репозиторий, CI, сеть или production. Запустите её в Node.js, передав изменение схемы, карту потребителей и описание возврата. Код проверяет только наличие трёх полей; он не делает вывод о совместимости клиентов.

\n
const 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: можно задать владельцу контракта конкретный вопрос, но ответ ещё должен опираться на исходный код, схему или контрактный тест.

\n

Три поля в примере — проектные. schemaDelta означает точную старую и новую форму, consumerMap — границу поиска и категории потребителей, rollbackNote — старую форму, порядок возврата и условия обратимости. В другом проекте минимальный набор будет иным. Важно сохранить правило: каждое требуемое поле должно быть связано с конкретным риском и способом проверки.

\n

Как проверить реальный pull request

\n

Начните с цели изменения и списка затронутых файлов. В документации GitHub отдельный просмотр файлов, комментарии на конкретных изменениях и отметка Viewed помогают не потерять часть diff. Это полезная операционная последовательность, но отметка Viewed не доказывает корректность кода. После неё всё равно нужна проверка свойства, ради которого меняли систему.

\n
  1. Сформулируйте симптом в наблюдаемой форме: какое поле, состояние, вход или побочный эффект изменился.
  2. Снимите старое и новое поведение из схемы, теста, логов или исходного кода; не подменяйте их пересказом описания PR.
  3. Составьте карту потребителей: UI, API-клиенты, фоновые задачи, миграции, операционные скрипты и внешние интеграции.
  4. Для каждого потребителя запишите ожидаемый ответ на null, отказ, повтор или недопустимый вход.
  5. Проверьте отрицательный путь отдельным тестом или воспроизводимой командой: ошибка должна менять только ожидаемое состояние и не дублировать побочный эффект.
  6. Сверьте утверждение с evidence. Если не хватает одной позиции, напишите stop с её точным именем.
  7. Если риск и evidence определены, передайте владельцу один вопрос с границей вывода и ожидаемым результатом.
  8. В итоговом комментарии разделите обязательные изменения и замечания типа polish; личное предпочтение не должно блокировать поведенческий вывод.
\n

Почему стиль вытесняет поведение

\n

Стиль легко обсуждать: строка видна, правило знакомо, исправление локально. Контракт требует контекста. Поэтому style-комментарий не плох сам по себе. Ошибка возникает, когда он закрывает обсуждение изменения поведения. Обязательное правило форматирования исправьте локально, а риск контракта вынесите отдельным блоком с доказательством и владельцем.

\n

Наличие теста также не равно проверке нужного свойства. Unit-тест может подтвердить, что функция вернула null, но не показывает, как значение трактуют старые потребители. Тест ветки ошибки может пройти, хотя повтор операции создаёт дубль, если побочный эффект выполняется до записи статуса. Руководство Google отдельно предлагает проверять edge cases и спрашивать, упадёт ли тест при поломке кода. Это хороший фильтр для фразы «тесты зелёные».

\n

Вместо общего комментария оставьте наблюдаемую формулировку: «в ответе поле стало nullable; для клиента A не найдено поведение при null». Такой комментарий содержит изменение, missing evidence и ожидаемого владельца. Он полезнее утверждения «API небезопасен», если проверка ещё не показала нарушение.

\n

Stop и escalation — разные действия

\n

Stop означает, что данных недостаточно даже для точного следующего вопроса. Если неизвестен класс риска, сначала опишите границу изменения. Если нет карты потребителей, запросите только её. Не пишите «проверьте всё»: такой запрос нельзя проверить и нельзя завершить.

\n

Escalation означает, что риск и нужное evidence названы, но решение принадлежит другой роли. Вопрос о форме ответа передают владельцу контракта, вопрос о повторе побочного эффекта — владельцу состояния или эксплуатации, вопрос о границе доверия — владельцу безопасности. Передача должна содержать риск, факты, точный вопрос и ограничение вывода. Она не должна приписывать получателю готовый диагноз.

\n

Корректные формулировки звучат слабее, но дают следующий шаг: «нужно проверить совместимость потребителей с новой nullable-формой», «нужно уточнить повтор операции после timeout», «нужно привлечь владельца trust boundary». Пока нет проверки, нельзя писать «все клиенты совместимы», «retry безопасен» или «уязвимость найдена».

\n

Ограничения применимости

\n

Модель не считает severity, не назначает SLA и не заменяет security-аудит, контрактные тесты, нагрузочные проверки или миграционный план. Она не доказывает отсутствие дефекта. Она ограничивает силу комментария теми фактами, которые реально собраны.

\n

Для финансовой операции в карту добавьте идемпотентность, аудит и правила сверки. Для публичного API — версию, период совместимости и план удаления старого поля. Для персональных данных — источник согласия, срок хранения и права доступа. Для конкурентного кода — интерливинг, блокировки и наблюдаемое состояние. Эти позиции нужны только там, где они принадлежат затронутой границе.

\n

Пример с assessContractEvidence намеренно мал. Он не моделирует распределённую транзакцию, не запускает миграцию и не возвращает production-метрики. Его проверяемое свойство уже: при отсутствии consumerMap функция возвращает stop и не объявляет совместимость доказанной. Переносить этот статус на реальные клиенты без отдельной проверки нельзя.

\n

Критерий готовности

\n

Review готово, когда читатель без догадок отвечает на четыре вопроса: какой симптом привёл к проверке, какая граница риска затронута, какое evidence подтверждает следующий вопрос и какое утверждение запрещено делать. Для неполного набора виден точный stop. Для полного набора есть владелец вопроса, отрицательный путь и критерий результата. Если остаются «выглядит хорошо», «тесты зелёные» или «потом проверим», поведенческий вывод ещё не закрыт.

\n

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

" } diff --git a/editorial/agent-rewrites/045.json b/editorial/agent-rewrites/045.json index 91d42dd..0cdeb98 100644 --- a/editorial/agent-rewrites/045.json +++ b/editorial/agent-rewrites/045.json @@ -3,5 +3,5 @@ "slug": "editorial-2026-10-practice-code-review-standard", "title": "Code review без шума: как проверить риск изменения", "excerpt": "Практический стандарт для code review: сначала назвать границу изменения и цену ошибки, затем запросить нужное доказательство и остановиться, если сильный вывод пока не подтверждён.", - "contentHtml": "

В pull request может быть двадцать комментариев, но ни одного вопроса о данных, миграции или отказе. Reviewer исправляет имя переменной и форматирование. В это время изменение меняет nullable-поле, порядок переходов состояния или правило доступа. Симптом виден сразу: обсуждение длинное, а главный риск не назван. Цена ошибки — несовместимый потребитель, повторная операция после timeout или доступ к данным не той роли. Такой дефект часто обнаруживается уже после слияния, когда исправление требует обратной миграции или ручного восстановления.

\n

Code review снижает риск только тогда, когда связывает границу изменения с проверяемым доказательством. Комментарий должен отвечать на четыре вопроса: что меняется, чем это опасно, какой факт сузит неопределённость и какое действие допустимо сейчас. Если факта не хватает, reviewer должен остановить вывод, а не заполнять пробел догадкой.

\n

Тезис: сначала граница, потом замечание

\n

Разделите изменение на риск-классы. Contract risk возникает, когда меняется форма данных или ожидание потребителя. Operational risk появляется при изменении timeout, retry, состояния, очереди или наблюдаемости. Security risk затрагивает доверенную сторону, правило входа и последствие злоупотребления. Style-only ограничен читаемостью и не меняет поведения.

\n

Класс риска не равен severity. Он выбирает первый вопрос. Для изменения контракта нужен schema delta, карта consumers и путь возврата. Для изменения поведения нужны переходы состояния и failure mode. Для границы доступа нужна модель доверия и проверка запрещённого входа. Для локального стиля достаточно короткого объяснения, почему код станет понятнее.

\n
\"Матрица
Существующая схема помогает выбрать вопрос к изменению. Она не заменяет запуск тестов и не выдаёт вердикт о конкретном pull request.
\n

Механизм: evidence ограничивает силу вывода

\n

Evidence — это именованный факт, который другой инженер может проверить в пределах задачи. Ссылка на файл не всегда является evidence. Три изменённых файла показывают объём diff, но не доказывают, что перечислены все потребители. Тест с зелёным статусом показывает проход конкретного сценария, но не объясняет, что произойдёт при повторе после отказа.

\n

Свяжите каждый факт с вопросом. schemaDelta отвечает, какое поле изменилось. consumerMap показывает, кто читает старую форму. rollbackNote описывает, что происходит при возврате. failureMode задаёт отрицательный путь. Такая связь важнее количества ссылок: один точный артефакт может закрыть вопрос, а десять общих ссылок — нет.

\n

Reviewer не обязан принимать формулу «это только рефакторинг». Попросите назвать invariant — свойство, которое не должно измениться, — и способ его проверить. Если invariant не назван, scope остаётся гипотезой. Положительный вывод не открывается.

\n

Учебный пример: карточка риска

\n

Ниже — учебный пример на TypeScript. Он не читает репозиторий и не утверждает результат настоящего review. Функция проверяет только полноту входной карточки. Её задача — не найти дефект автоматически, а не дать написать «можно одобрять», когда отсутствует обязательная граница.

\n
type 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.

\n

Симптом → причина → проверка → действие

\n
Диагностика замечаний в code review
СимптомПричинаПроверкаДействие
Много комментариев о стиле, риск не названВсе замечания получили один приоритетОтметить, меняется ли поведение или контрактВынести риск в отдельный комментарий
«Все клиенты совместимы» без спискаВывод подменил карту потребителейНайти владельцев и места чтения старой формыОстановить вывод и запросить consumer map
Тест зелёный, но retry не описанПроверен happy pathПроследить переход после timeout и повторной попыткиДобавить failure case или оставить stop
«Это только рефакторинг»Не назван invariantСравнить вход, выход и побочные эффекты до и послеПопросить invariant и способ проверки
Комментарий звучит как приказ, но не объясняет рискНормативное слово заменило аргументСпросить, какое свойство защищает требованиеПереписать комментарий через факт и действие
\n

Как писать сильный комментарий

\n

Начните с наблюдаемого факта. «Поле discount стало nullable» точнее, чем «изменение опасное». Затем назовите последствие: «клиент, который распаковывает значение без проверки, получит ошибку». После этого укажите проверку: «найдите все consumers старой схемы и покажите обработку null». Завершите действием: «до этой проверки не делаем вывод о совместимости».

\n

Один комментарий должен вести к одному действию. Не смешивайте обязательный вопрос о контракте с необязательным предложением переименовать функцию. Метка request-contract-evidence говорит о границе данных. Метка style-note говорит о читаемости. Автор может ответить на них разными изменениями и не потеряет важный риск среди косметических правок.

\n

Для security и эксплуатации требуйте владельца вопроса, если сами не можете проверить границу. Reviewer может заметить, что endpoint принимает роль из тела запроса, но не должен объявлять всю модель доступа безопасной без контекста авторизации. Точный комментарий выглядит так: «Роль приходит из недоверенного входа. Где сервер связывает её с authenticated user? Нужен путь проверки отрицательного случая». Это уже проверяемый вопрос.

\n

Порядок действий

\n
  1. Опишите симптом и цену ошибки одним предложением.
  2. Найдите границу изменения: данные, состояние, доступ, наблюдаемость или только стиль.
  3. Назовите риск-класс и invariant, который должен сохраниться.
  4. Сформулируйте один вопрос, ответ на который изменит решение.
  5. Запросите минимальное evidence: schema delta, consumer map, failure mode, access rule или другой конкретный артефакт.
  6. Проверьте отрицательный путь, а не только успешный сценарий.
  7. Разделите обязательное исправление, уточняющий вопрос и необязательную style-note.
  8. Если evidence отсутствует, верните точный stop без предположения о причине.
  9. Если evidence есть, сделайте только тот вывод, который оно поддерживает; совместимость, approval и выпуск проверяются отдельно.
\n

Ограничения стандарта

\n

Матрица не заменяет тестирование, threat model, дизайн-документ, миграционный план или наблюдаемость. Она не перечисляет всех возможных рисков и не назначает единственный порядок приоритетов. В маленьком style-only изменении запрос consumer map создаст ритуал без пользы. Поэтому классификация тоже должна опираться на invariant и границу поведения.

\n

Даже полная карта потребителей не доказывает, что каждый путь проверен. Она показывает область поиска. Результат зависит от статического анализа, динамической маршрутизации, конфигурации и скрытых интеграций. Если список получен неполным способом, так и напишите. Честный stop лучше уверенного «совместимо».

\n

Стандарт также не решает спор о продуктовой цели. Изменение может быть технически аккуратным, но не соответствовать требованиям продукта или политики безопасности. В таком случае reviewer фиксирует технические факты и передаёт вопрос владельцу решения. Code review не превращает полномочия reviewer в полномочия архитектора или владельца риска.

\n

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

\n

Review-вопрос готов, если другой инженер может быстро назвать границу изменения, цену ошибки, нужное evidence, отрицательный путь и следующее действие. В тексте нет вывода сильнее, чем подтверждающие факты. Для каждого обязательного замечания указан владелец проверки или понятный способ её выполнить. Косметический комментарий не маскирует контрактный, эксплуатационный или security-риск.

\n

Проверьте это на одной карточке. Если читатель не может ответить, какой факт переведёт stop в следующий шаг, карточка не готова. Если ответ есть, это ещё не разрешение на слияние. Это только ясная граница между тем, что уже видно, и тем, что нужно проверить.

\n

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

" + "contentHtml": "

В pull request может быть двадцать комментариев, но ни одного вопроса о данных, миграции или отказе. Reviewer исправляет имя переменной и форматирование. В это время изменение меняет nullable-поле, порядок переходов состояния или правило доступа. Симптом виден сразу: обсуждение длинное, а главный риск не назван. Цена ошибки — несовместимый потребитель, повторная операция после timeout или доступ к данным не той роли.

\n

Хорошее review не обязано находить все дефекты и не обещает идеальный код. Его задача уже выполнена, если оно связывает наблюдаемый факт с границей изменения, проверкой и допустимым действием. Если факта не хватает, reviewer ограничивает вывод: не пишет «совместимо» или «безопасно», а называет, какой вопрос ещё требуется закрыть.

\n

Что именно проверяет review

\n

Начните с цели изменения, а не с первой строки diff. Что должен получить пользователь, потребитель API или оператор? Затем проверьте дизайн, поведение, сложность, тесты, имена, комментарии, стиль и документацию. Такой порядок совпадает с областями, перечисленными в руководстве Google Engineering Practices, но он не является универсальной политикой: команда может добавить свои требования к миграциям, данным или доступам.

\n

Разделите обязательное и необязательное. Нарушенная граница контракта — причина для точного вопроса или остановки. Неудачное имя, которое не меняет смысл и не противоречит style guide, — отдельная рекомендация. Когда оба типа замечаний лежат в одном списке без меток, автор тратит внимание на косметику, а дорогой риск выглядит равным запятой.

\n

Сначала назовите границу изменения

\n

У любого diff есть граница, через которую меняется ожидание другой части системы. Для контракта это форма JSON, тип поля, код ошибки или порядок вызовов. Для поведения во времени — состояния, timeout, retry и побочный эффект. Для доступа — доверенная сторона, входное правило и запрещённый результат. Для style-only изменения граница остаётся локальной: меняется читаемость, но не наблюдаемое поведение.

\n

Класс риска выбирает следующий вопрос. При nullable-поле сравните старую и новую форму, найдите потребителей и опишите переход. При retry покажите состояние до сбоя, событие, состояние после него и результат повтора. При проверке роли отделите значение из запроса от решения, кто имеет право его передать. Фраза «это только рефакторинг» не закрывает проверку: попросите назвать invariant — свойство, которое должно остаться прежним, — и способ его проверить.

\n
\"Последовательность
Схема помогает идти по соседним границам: намерение, запрос, ответ и вход рендера. Она не заменяет чтение diff и не доказывает корректность конкретного изменения.
\n

Evidence ограничивает силу вывода

\n

Evidence — именованный факт, который другой инженер может проверить в рамках задачи. Ссылка на файл показывает место, но не обязательно показывает всех потребителей. Зелёный тест подтверждает один сценарий, но не объясняет повтор после timeout. Три изменённых файла показывают объём diff, но не доказывают, что откат возможен.

\n

Привяжите каждый риск к минимальному набору доказательств. Для контракта это schemaDelta, consumerMap и rollbackNote. Для поведения — stateTransition, failureMode и граница наблюдения. Для доступа — trustBoundary, правило входа и последствие нарушения. Не просите «проверить всё»: такой запрос нельзя завершить и нельзя воспроизвести.

\n

Отделяйте факт от вывода. «Поле стало nullable» — факт. «Все клиенты готовы» — вывод, для которого нужна карта клиентов и проверка их обработки null. «Тест прошёл» — факт о запуске. «Повтор безопасен» — более сильное утверждение, которое требует отрицательного сценария. Если набор неполон, корректный результат — stop с конкретным missing evidence.

\n

Матрица симптома и действия

\n
Как превратить замечание в воспроизводимый вопрос
Наблюдаемый симптомРискМинимальная проверкаДопустимое действие
Поле ответа стало nullableКонтрактСравнить формы и перечислить потребителей старого типаЗапросить карту потребителей и обработку null
После timeout операция повторяетсяПоведение во времениПроследить state до сбоя, повтор и побочный эффектОставить вопрос до проверки идемпотентности
Роль приходит из тела запросаГраница доверияНайти серверную связь роли с authenticated userПередать узкий вопрос владельцу доступа
В обсуждении только форматированиеРиск вытеснен стилемПроверить цель, вход, выход и invariantРазделить обязательный риск и style-note
Тест зелёный, но проверен только happy pathЛожное покрытиеСломать условие и убедиться, что тест падаетДобавить отрицательный сценарий или ограничить вывод
\n

Воспроизводимый пример: карточка review

\n

Ниже — небольшой TypeScript-валидатор. Он не читает репозиторий, не запускает CI и не оценивает production. Функция проверяет только полноту карточки: если для контрактного изменения нет карты потребителей, она возвращает stop. Это полезный механизм для шаблона комментария, но не автоматическое разрешение слияния.

\n
type 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. Даже тогда карточка не доказывает, что каждый потребитель действительно обработан. Она лишь делает следующий вопрос явным.

\n

Как читать diff по шагам

\n
  1. Сформулируйте наблюдаемый симптом и цену ошибки: что изменится для пользователя, потребителя или оператора.
  2. Найдите границу: данные, состояние, доверие, наблюдаемость или только локальный стиль.
  3. Сравните старое и новое поведение. Для контракта запишите schema delta, для retry — state transition, для доступа — trust boundary.
  4. Назовите invariant и отрицательный путь. Примеры: старый клиент не падает, повтор не создаёт дубль, запрещённый вход получает отказ.
  5. Соберите минимальное evidence и укажите, какой вопрос закрывает каждая позиция.
  6. Пройдите связанные файлы и тесты в контексте, а не только изменённые строки. Если область проверки неполна, напишите это прямо.
  7. Сформулируйте одно действие: исправить, показать evidence, уточнить у владельца или остановить вывод.
  8. После ответа автора проверьте именно заявленное свойство. Зелёный CI не отменяет ручную проверку контракта, границы доступа или поведения при повторе.
\n

Как писать комментарий, который помогает

\n

Начните с факта: «Поле discount стало nullable». Затем назовите последствие: «старый потребитель может распаковать значение без проверки». Дайте проверку: «покажите список потребителей и тест обработки null». Завершите границей вывода: «до этого нельзя утверждать совместимость». В таком комментарии есть наблюдение, причина запроса и следующее действие.

\n

Один комментарий — одно решение. Обязательное замечание о контракте не прячьте среди предложения переименовать функцию. Для локального стиля используйте явную метку вроде Nit или «необязательно», если это соответствует правилам вашей системы review. В руководстве Google такая маркировка отделяет пожелание от требования; это снижает риск, что автор примет личное предпочтение за блокирующее условие.

\n

Если вопрос требует другой компетенции, передайте его владельцу границы, но не приписывайте ему диагноз. Для безопасности это может быть вопрос о модели доверия, для эксплуатации — о повторе побочного эффекта, для контракта — о совместимости потребителей. Передача должна содержать факты, точный вопрос и известное ограничение. Approval, merge и выпуск — отдельные решения, а не следствие одной заполненной карточки.

\n

Ограничения применимости

\n

Эта схема не заменяет дизайн-документ, threat model, контрактные тесты, нагрузочную проверку, аудит доступа или план миграции. Она не доказывает отсутствие уязвимости и не считает severity. Её функция скромнее: не позволить выводу стать сильнее доступных фактов.

\n

Для style-only изменения карта потребителей создаст лишнюю процедуру, если граница поведения действительно доказанно не меняется. Для финансовой операции потребуются дополнительные строки об идемпотентности, аудите и сверке. Для публичного API важны версия, период совместимости и план удаления старой формы. Для персональных данных добавьте права, срок хранения и путь удаления. Расширяйте матрицу только теми условиями, которые принадлежат конкретному риску.

\n

Есть и предел статического review. Скрытая динамическая маршрутизация, конфигурация, внешняя интеграция и race condition могут находиться за пределами доступного diff. В этом случае reviewer фиксирует область, которую проверил, и остаточный вопрос. Честный stop полезнее уверенного «безопасно», если подтверждающего эксперимента ещё нет.

\n

Критерий готовности

\n

Review-вопрос готов, когда другой инженер без догадок видит симптом, границу, цену ошибки, нужное evidence, отрицательный путь и следующее действие. Для обязательного замечания понятен владелец проверки. Для style-note ясно, что она не блокирует поведение. Для положительного результата указано, что именно проверено и какое утверждение всё ещё запрещено.

\n

Перед отправкой комментария перечитайте его как короткую карточку: факт → риск → доказательство → действие → ограничение. Если в ней осталось «выглядит хорошо», «всё совместимо» или «тесты зелёные» без конкретного свойства, вернитесь к границе изменения. Это не формальность: так обсуждение остаётся воспроизводимым после смены автора и reviewer.

\n

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

" } diff --git a/editorial/agent-rewrites/046.json b/editorial/agent-rewrites/046.json index 47a4247..023f83f 100644 --- a/editorial/agent-rewrites/046.json +++ b/editorial/agent-rewrites/046.json @@ -1,7 +1,7 @@ { "index": 46, "slug": "editorial-2026-09-field-frontend-backend-boundary", - "title": "Когда UI и API расходятся: как найти нарушенную границу", - "excerpt": "Кнопка сообщает об успехе, экран показывает старое состояние, а API отвечает иначе. Разбираем четыре наблюдения, порядок проверки и границу, после которой нельзя делать выводы.", - "contentHtml": "

Пользователь нажимает «Сохранить», видит сообщение об успехе, а после обновления страницы получает старые данные. В 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. Не нужно писать в лог тело ответа целиком. Идентификатор и хэш нормализованного класса часто связывают события без копирования персональных данных.

\"Четыре
Сравнивайте соседние границы. Ответ из Network не доказывает, что тот же объект получил компонент.
Симптомы на границе frontend и backend
СимптомВероятная причинаПроверкаДействие
На экране старое значение, ответ содержит новоеАдаптер или 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, статью или задачу.

Как отличить cache от race

Кеш обычно даёт повторяемость: один и тот же ключ возвращает прежнюю версию. Сравните request key, заголовки кеша, revision и источник данных. Не называйте кеш причиной, пока повторный запрос с новым ключом не меняет наблюдение.

Гонка зависит от порядка. Запрос A ушёл первым, B — вторым, но B вернулся раньше. Если store принимает ответы без проверки версии или актуальности запроса, A перезапишет более новое состояние. В тесте задержите только один ответ. Если результат меняется вместе с задержкой, гипотеза о race получила проверку.

Отдельно проверьте отмену запроса. Компонент мог размонтироваться, adapter мог получить AbortError, а локальный optimistic patch остался. В этом случае отсутствие response не доказывает отказ backend. Оно означает только, что текущий слой не получил наблюдаемого ответа.

Порядок расследования

  1. Запишите симптом, имя операции, класс безопасного входа и request id. Не начинайте с предположения о кеше.
  2. Снимите method, route template, статус и Content-Type. Для mutation сначала проверьте идемпотентность и не запускайте повтор вслепую.
  3. Проверьте response по контракту. Разделите transport failure, domain rejection и успешный ответ без representation.
  4. Сравните response с render input: status, allowed actions, message code и version.
  5. Если значения расходятся, проверьте adapter, cache key, optimistic patch и порядок ответов.
  6. Если значения совпадают, перейдите к selector, memoization, hydration или локальному состоянию компонента.
  7. Сформулируйте один следующий тест, который различает оставшиеся гипотезы. Меняйте код только после проверки.

Когда расследование нужно остановить

Остановитесь, если следующий вывод требует неполученных данных. Так бывает, когда нужен 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-результата.

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

" + "title": "Граница frontend и backend: как доказать, где расходится правило скидки", + "excerpt": "Браузер показывает скидку, сервер считает другую сумму, а повторный запрос только запутывает расследование. Разбираем воспроизводимый кейс, контракт расчёта, статусы HTTP и безопасный порядок проверки.", + "contentHtml": "

Самый опасный дефект на границе frontend и backend выглядит убедительно с обеих сторон. JavaScript показывает покупателю скидку 10%, кнопка становится активной, а сервер при оформлении заказа возвращает полную стоимость. В другом варианте браузер получает ответ 200 и рисует старую скидку из локального состояния. Если сразу переписать условие в одном месте, можно скрыть симптом и оставить два разных правила.

\n

Ниже — учебный кейс с фиксированными тестовыми данными, а не отчёт о конкретной production-системе. У корзины есть промокод SAVE10. Клиент предварительно показывает скидку для суммы от 5 000 рублей. Backend дополнительно проверяет категорию товара и актуальность корзины. Редкий сценарий: сумма подходит, но один товар исключён из акции. Локальная функция показывает 600 рублей скидки, сервер возвращает решение rejected и ноль.

\n

Главный вывод практический: денежный результат и право применить скидку должны иметь одного авторитетного владельца. Frontend может дать быстрый preview (предварительный расчёт), но не должен выдавать его за подтверждённое состояние. Чтобы найти нарушенную границу, нужно связать намерение пользователя, исходный запрос, ответ сервера, адаптированную модель и вход компонента в рендер. Если один переход не зафиксирован, причина остаётся гипотезой.

\n

Сначала разделите preview и подтверждённый расчёт

\n

У интерфейса есть две разные задачи. Preview отвечает на вопрос «что примерно произойдёт, если текущие условия сохранятся». Подтверждённый расчёт отвечает на вопрос «какую сумму система разрешает использовать в следующей операции». Эти значения могут совпадать, но это не одно и то же поле.

\n

В учебном контракте браузер отправляет состав корзины и код акции. Сервер проверяет цены, доступность, права на акцию, исключения по категориям и текущую ревизию корзины. Ответ содержит сумму в копейках, решение и код причины. Клиент не пересчитывает ответ, а отображает его. Так правило не дублируется в двух исполнителях.

\n
Схема расследования расхождения скидки между frontend и backend: intent, request, response и render input связаны идентификатором запроса
Проверяйте четыре соседних перехода. Снимок интерфейса показывает только финальный render input и не доказывает, какой ответ его породил.
\n

Отдельное поле previewDiscountCents полезно только при явно описанном статусе «предварительно». Поле discountCents в подтверждённой модели должно приходить из ответа сервера. Если оба значения отображаются рядом, подпишите источник и момент расчёта, иначе пользователь увидит число без его условий применимости.

\n

Воспроизводимый сценарий с одной редкой веткой

\n

Для проверки возьмём корзину из одного товара. Все числа ниже — тестовый fixture (фиксированный набор входов), их можно перенести в unit- или contract-тест. Цена хранится в копейках, чтобы пример не зависел от округления чисел с плавающей точкой.

\n
POST /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 и показать причину, если такой код предусмотрен интерфейсным контрактом.

\n

Второй прогон меняет только категорию на electronics. Если политика акции разрешает её, ожидаемое решение — accepted, скидка 60 000 и итог 540 000 копеек. Третий прогон меняет cartRevision на устаревшее значение. Он нужен, чтобы отделить расхождение бизнес-правила от конфликта состояния. Нельзя считать эти три случая одной ошибкой «скидка не работает».

\n
Минимальная матрица воспроизведения границы
ВходОжидаемое решение backendЧто может ошибочно показать UIПроверка
600 000 копеек, gift-card, SAVE10rejected, скидка 0Скидка 60 000 по локальному порогу суммыСопоставить response с render input
600 000 копеек, electronics, SAVE10accepted, скидка 60 000Старая модель после смены товараПроверить cache key и отмену прошлого запроса
Устаревшая cartRevisionКонфликт по текущему состояниюУспех из optimistic UIЗаписать порядок запросов и статус ответа
Невалидный ответ без decisionОстановка адаптераТихая подстановка прежней скидкиПроверить schema validation и отрицательный тест
\n

Матрица полезна тем, что меняет одну причину за раз. Если одновременно менять промокод, состав корзины и ревизию, положительный или отрицательный результат не подскажет, какая граница нарушена.

\n

Проверяйте не только статус, но и representation

\n

В Fetch свойство Response.ok означает только статус из диапазона 200–299. Поэтому response.ok === true не доказывает, что тело содержит именно модель расчёта. Для endpoint, который должен вернуть JSON-котировку, проверяйте ожидаемый статус, Content-Type и обязательные поля отдельно.

\n

Статус 204 означает успешное выполнение без содержимого ответа. Он может быть правильным для команды, после которой клиент сам перечитывает ресурс, но не заменяет JSON-ответ котировки. Статус 202 означает, что запрос принят в обработку, а обработка ещё не завершена; его нельзя трактовать как подтверждённую сумму. Статус 409 описывает конфликт с текущим состоянием целевого ресурса и подходит для отдельного сценария устаревшей корзины, если это согласовано контрактом.

\n

Клиентская ветка должна различать транспортную ошибку, отказ доменного правила и невалидную representation (представление ресурса). Сетевой сбой не дал ответа. Отказ промокода дал ответ с понятным решением. Невалидная схема говорит, что текущая версия клиента не может безопасно применить контракт. У всех трёх случаев разный следующий шаг.

\n
type 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

Найдите точку расхождения по четырём снимкам

\n

Начните с intent — намерения пользователя: товар выбран, промокод введён, пользователь нажал «Рассчитать». Затем сохраните нормализованный request: метод, шаблон маршрута, безопасный идентификатор запроса, ревизию корзины и хэш набора SKU. Секреты, полные персональные данные и платёжные реквизиты в диагностический контекст не входят.

\n

Третий снимок — response. Зафиксируйте статус, Content-Type, коды решения, ревизию и ETag, если сервер его отдаёт. Не нужно копировать тело целиком: для расследования достаточно разрешённого набора полей. Четвёртый снимок — render input, то есть объект, который действительно получил selector или компонент. Network-панель сама по себе не показывает этот объект.

\n

Если response содержит discountCents: 0, а render input содержит 60 000, ищите ошибку в адаптере, store, cache key или optimistic patch. Если render input уже равен нулю, а экран показывает 60 000, переходите к selector, memoization, локальному state или hydration. Если request не содержит категорию, backend не обязан восстановить её из догадки клиента: это дефект request contract.

\n

Для каждого снимка используйте один корреляционный ключ, например test-quote-046. Это не стандарт HTTP и не замена распределённой трассировке, а договорённость диагностического сценария. Ключ связывает события, но не доказывает причинность: порядок и содержимое переходов всё равно нужно проверить.

\n

Отделите cache от race и устаревшей ревизии

\n

Кеш даёт обычно повторяемый результат для одного ключа: тот же запрос получает ту же старую representation. Для проверки сравните URL, параметры, заголовки кеша, ревизию и ETag. Новый URL с добавленным случайным параметром — плохой диагностический инструмент, если он меняет контракт и не отражает настоящий путь приложения.

\n

Гонка зависит от порядка. Запрос A отправлен для electronics, запрос B — после изменения товара для gift-card. Если B вернулся первым, а A пришёл позже, устаревший A может перезаписать store. Искусственно задержите только один ответ в тестовом сервере и запишите последовательность. Если результат меняется вместе с задержкой, гипотеза о race получила воспроизводимую проверку.

\n

ETag и If-Match решают другой класс задачи. HTTP определяет ETag как валидатор representation, а If-Match позволяет условно выполнять изменение и предотвращать потерянную запись. Это полезно для обновления корзины или применения команды, но не превращает любой локальный cartRevision в HTTP-валидатор. Сопоставьте оба поля только после явного описания их владельца и жизненного цикла.

\n

При несовпадении ревизии сервер может вернуть 412 для проваленной предварительной проверки или 409 для конфликта состояния — точный выбор задаёт контракт. Клиент должен показать действие: перечитать корзину, пересчитать котировку или попросить повторить после подтверждения. Автоматический повтор команды с неизвестной идемпотентностью может создать второй побочный эффект.

\n

Порядок расследования и исправления

\n
  1. Опишите один симптом с числами и условиями: «при категории gift-card preview показывает скидку 60 000 копеек, а quote возвращает 0».
  2. Зафиксируйте fixture: вход корзины, промокод, ревизию, ожидаемый статус, тело ответа и ожидаемый render input.
  3. Проверьте request до backend. Убедитесь, что в нём есть все поля, влияющие на бизнес-решение, а сериализация не теряет категорию или ревизию.
  4. Проверьте status, Content-Type и representation. Не называйте ответ успешным только из-за завершившегося Promise или свойства ok.
  5. Сравните response с моделью после adapter и с объектом, который получил компонент. Так локализуется первая граница расхождения.
  6. Повторите сценарий с одной изменённой переменной: категория, ревизия, порядок ответов или cache key. Не смешивайте эксперименты.
  7. Исправьте владельца правила. Backend остаётся источником подтверждённой суммы; frontend удаляет дублирующее условие или помечает его только как preview.
  8. Добавьте отрицательные тесты: исключённая категория не получает скидку, устаревший ответ не перезаписывает новый, невалидное тело не сохраняет старую модель.
  9. Проверьте интерфейс после обновления страницы. Успешный первый рендер не доказывает, что состояние переживает повторное чтение.
\n

Если правило меняется часто, храните условия акции в одном контракте или сервисе, а не копируйте их в два языка. Если быстрый preview нужен для отзывчивости, верните ему ограниченный статус и замените его подтверждённой котировкой после ответа. Выигрыш в скорости интерфейса не оправдывает два независимых источника суммы.

\n

Как оформлять отказ, чтобы не маскировать причину

\n

Для прикладного отказа API может использовать формат Problem Details с медиа-типом application/problem+json. RFC 9457 описывает поля вроде type, title, detail и расширения для конкретного типа проблемы. Такой формат помогает адаптеру отличить ожидаемый отказ промокода от транспортной ошибки, но не диктует тексты для UI и не разрешает раскрывать внутренние детали.

\n
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 нельзя без фильтра показывать пользователю или отправлять в общий лог: они могут содержать идентификаторы, внутренние маршруты или данные, которые не нужны для принятия решения.

\n

На frontend полезно иметь явное отображение: «Корзина изменилась — обновите расчёт», «Промокод не действует для выбранного товара» и «Не удалось получить расчёт». Это разные действия. Общий toast «ошибка сети» заставит пользователя повторять запрос, хотя сервер уже вернул корректный отказ по бизнес-условию.

\n

Ограничения применимости

\n

Этот способ подходит для синхронного HTTP-расчёта, где можно получить request, response и render input. Он не решает сам по себе асинхронную обработку очередью: для неё нужен отдельный статус операции, политика повторов и источник истины о завершении. Он также не заменяет авторизацию, аудит денежных операций, contract testing или проверку округления на сервере.

\n

Нельзя переносить правило «backend всегда прав» на отображение, которое сознательно является предварительным прогнозом. Preview может быть полезен, если пользователь видит его статус, а окончательная операция повторно проверяет условия. Нельзя считать ETag защитой от повторной покупки, если endpoint не описывает идемпотентность команды. Нельзя использовать тестовые идентификаторы и фиктивные type URI как production-конфигурацию.

\n

Если нет доступа к телу ответа или к безопасному воспроизведению, остановите сильный вывод. Скриншот показывает симптом, но не доказывает источник числа. Один лог backend показывает обработку запроса, но не доказывает, какой объект получил компонент. В таком случае запросите обезличенный response, request id и минимальный тестовый fixture, а не исправляйте случайный слой.

\n

Критерий готовности

\n

Исправление границы готово, когда fixture с исключённой категорией и fixture с разрешённой категорией дают разные, ожидаемые решения; подтверждённая сумма приходит из одного владельца; response и render input можно связать безопасным идентификатором; устаревший ответ не меняет новую модель; невалидный контракт не сохраняет прежнюю скидку; после обновления страницы отображается то же подтверждённое состояние.

\n

Это проверяемый критерий, а не обещание отсутствия всех ошибок. Перед выпуском отдельно проверьте денежное округление, права на акцию, кеширование, повтор команды и наблюдаемость. Если хотя бы одно из этих условий не входит в тестовый стенд, зафиксируйте границу результата и не называйте локальный прогон доказательством всей системы.

\n

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

\n" }