diff --git a/editorial/agent-rewrites/119.json b/editorial/agent-rewrites/119.json index 2026ba0..a05ea94 100644 --- a/editorial/agent-rewrites/119.json +++ b/editorial/agent-rewrites/119.json @@ -1,7 +1,7 @@ { "index": 119, "slug": "editorial-2024-09-mechanism-adr-decisions", - "title": "ADR без иллюзии выбора: как записать решение, его цену и путь назад", - "excerpt": "Архитектурное решение теряет смысл, если в записи виден только победивший вариант. Разбираем, как сравнить альтернативы по одной схеме, проверить отрицательный путь и оставить условие для пересмотра.", - "contentHtml": "

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

\n

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

\n

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

\n

Механизм: от симптома к решению

\n

ADR полезен как короткая цепочка причин. Сначала автор отделяет факт от объяснения. Факт можно наблюдать: caller должен получить статус, пока фоновая операция продолжается; данные нельзя отдавать старше заданной границы; изменение должно откатываться без записи нового формата. Гипотеза объясняет, почему текущий путь не подходит. Решение отвечает на ограничение, а не на абстрактную цель вроде «улучшить архитектуру».

\n

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

\n

Статус помогает не смешивать обсуждение и историю. Proposed означает, что запись готова к проверке. Accepted фиксирует принятое решение. Superseded означает, что новый ADR заменил старый и объяснил причину. Статус не запускает миграцию, не назначает approval и не проверяет rollback автоматически. Эти действия должны иметь отдельного владельца и отдельный сигнал.

\n
symptom      = caller не получает понятный status\nconstraint   = acknowledgement не должен зависеть от business completion\nalternatives = direct response | bounded async state | shared workflow\ndecision     = bounded async state у границы сервиса\nprice        = expiry, owner состояния, cleanup и отдельная проверка retry\nreassess     = меняется контракт caller или срок хранения состояния
\n

Этот фрагмент — учебный пример формы. Он не описывает реальный сервис, трафик, SLO или production-результат. Его задача — показать, как решение связывает симптом, условие, цену и отрицательный путь. В настоящем ADR вместо учебных утверждений нужны ссылки на контракт, issue, тест, threat model или другой разрешённый источник evidence.

\n

Как сравнить альтернативы честно

\n

Сначала выровняйте уровень вариантов. Нельзя сравнивать «локальный cache» с «переделать платформу»: это разные масштабы и владельцы. Сформулируйте варианты на уровне решения, а затем укажите реализацию как следствие. Для asynchronous boundary это могут быть прямой ответ после завершения работы, bounded state с выдачей статуса и общий workflow с отдельным хранением состояния.

\n

Constraint fit отвечает на вопрос «закрывает ли вариант обязательное условие». Reversibility описывает не наличие кнопки undo, а область возврата, порядок действий и владельца. Evidence fit показывает, какие факты поддерживают выбор и какой вопрос остался открытым. Operating cost называет долг: expiry, retry, cleanup, миграцию, поддержку контракта или ручной review. Reassessment показывает, что должно измениться, чтобы открыть новый ADR.

\n
КритерийВопрос к каждому вариантуПроверяемый артефактЧего нельзя утверждать
Constraint fitКакое объявленное условие выполняется?Контракт, problem statement или acceptance questionЧто вариант оптимален для всех целей
ReversibilityКакой scope возврата, кто его выполняет и что останавливает change?План rollback и граница затронутого состоянияЧто rollback уже проверен в production
Evidence fitКакой факт поддерживает выбор и чего пока не хватает?Ссылка на источник, controlled test или явное unknownЧто отсутствие данных равно безопасному результату
Operating costКто поддерживает status, expiry, retry, cleanup или migration?Owner и consequence в ADRЧто стоимость измерена в часах или деньгах
ReassessmentКакой сигнал отменяет исходное предположение?Review condition, metric question или contract linkЧто дата сама запустит пересмотр
\n

Таблица не требует единой оценки. Она делает пропуски видимыми. Если у одного варианта есть контракт, а у другого только слово «сложно», сравнение ещё не началось. Если команда всё же применяет score, заранее закрепите шкалу, веса и смысл баллов. Рядом напишите, какие данные модель не учитывает. Итоговый балл может быть tie-breaker для обсуждения, но не доказательством корректности.

\n
\"Синтетическая
Иллюстрация показывает синтетическую матрицу. Значения помогают увидеть форму сравнения и не являются метриками реальной команды или production-системы.
\n

Пример записи с отрицательным путём

\n

Предположим, synchronous caller ждёт результат долгой операции. Прямой ответ сохраняет простую модель, но не выдерживает границу времени. Shared workflow даёт общий status, но добавляет cross-service owner и отдельный контракт. Bounded state у границы сервиса отделяет acknowledgement от business completion. Это может закрыть исходное условие, если срок хранения, повтор запроса и очистка состояния определены явно.

\n
## Decision\nВыбираем bounded state у границы сервиса.\n\n## Consequences\nПоложительные: caller получает отдельный status.\nЦена: owner хранит expiry и cleanup; retry должен быть идемпотентным.\n\n## Не делаем\nНе вводим shared workflow и не считаем локальное состояние\nуниверсальным механизмом координации.\n\n## Reassessment\nОткрываем новый ADR, если caller требует общего status между\nсервисами или срок хранения превышает допустимую границу.
\n

Отрицательный путь защищает запись от незаметного расширения. Без него локальное решение постепенно начинает обслуживать новые callers, а временное поле превращается в общий протокол. Если новое требование действительно появилось, это не повод молча дописывать старый ADR. Нужен successor с новым контекстом, альтернативами и ценой. Старый record остаётся историей прежнего компромисса.

\n

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

\n
СимптомПричинаПроверкаДействие
Выбранный вариант подробно описан, остальные названы «сложными»Автор сравнил не варианты, а привлекательность собственного решенияЗаполнить одну карточку criteria для каждой альтернативыПереписать ADR и назвать explicit downside победителя
После merge спорят, что на самом деле означал ADRВ записи смешаны факт, гипотеза и implementation detailПометить источник каждого важного утвержденияВынести неизвестное в evidence gap и добавить owner проверки
Rollback существует только как фразаНе определены scope, порядок и stop conditionПровести dry run на учебной копии или описать точную границуСвязать ADR с отдельным rollback-планом и не объявлять его проверенным без evidence
Локальный механизм используют новые callersОтрицательный путь и граница применимости не записаныПроверить список потребителей и контракт состоянияОстановить расширение, создать successor ADR при новом требовании
Дата review прошла, но никто не вернулся к решениюДата ошибочно принята за автоматический процессНайти владельца сигнала и фактическое событие пересмотраНазначить действие отдельно или заменить дату проверяемым trigger
\n

Порядок работы

\n
  1. Сузьте вопрос. Опишите одну границу: какой caller, какое состояние и какое обязательное условие создают проблему.
  2. Зафиксируйте наблюдаемый симптом и цену. Отделите факт от гипотезы. Назовите, что станет дороже или опаснее при неверном выборе.
  3. Соберите три разумные альтернативы. Приведите их к одному уровню и не добавляйте вариант, который не может закрыть constraint.
  4. Заполните одинаковые поля. Укажите constraint fit, reversibility, evidence gap, operating cost, owner и отрицательный путь.
  5. Сформулируйте decision как компромисс. Запишите, что реализуем и чего намеренно не делаем.
  6. Назначьте проверяемый сигнал. Это может быть изменение контракта, новый потребитель, исчерпание срока хранения или конкретный вопрос к метрике.
  7. Проверьте запись до implementation. Reviewer должен суметь назвать выбранный вариант, его цену и условие пересмотра, не открывая код.
  8. Свяжите запись с реализацией отдельно. ADR не заменяет тест, threat model, migration plan, benchmark, runbook или incident review.
\n

Ограничения и критерий готовности

\n

ADR не делает решение истинным и не заменяет исследование. Он не измеряет latency, надёжность, стоимость или безопасность, если рядом нет соответствующего evidence. Он не гарантирует, что команда найдёт все альтернативы. Формат может различаться: важнее сохранить контекст, выбор, статус, последствия, границы и путь пересмотра. Для legal, security, privacy и data boundary нужны отдельные проверки с отдельными владельцами.

\n

Не добавляйте в учебный пример вымышленные traffic, error rate, savings или outcome. Если число нужно для объяснения score, пометьте его как синтетическое и не переносите вывод за пределы модели. Если production-данных нет, честная формулировка — «данных пока недостаточно», а не «вариант доказанно безопасен».

\n

Запись готова, когда независимый читатель может ответить на пять вопросов: какой симптом наблюдали; какое constraint обязателен; почему сравнивали именно эти варианты; какую цену принимает выбранный путь; какой сигнал заставит открыть новое решение. Проверка проста: уберите из текста заголовок Decision и попросите коллегу восстановить его из context, alternatives и consequences. Если он не может назвать отрицательный путь или owner, ADR ещё не готов.

\n

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

\n" + "title": "ADR как журнал компромисса: решение, цена и путь назад", + "excerpt": "Хороший ADR сохраняет не только выбранный вариант, но и исходное ограничение, отклонённые альтернативы, цену решения и сигнал для пересмотра. Разбираем это на сценарии долгого экспорта.", + "contentHtml": "

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

\n

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

\n

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

\n

Что именно фиксирует ADR

\n

Официальное сообщество ADR определяет architectural decision как обоснованный выбор, значимый для архитектуры, а ADR — как запись одного такого выбора и его rationale, то есть причины. Коллекция записей образует decision log. Поэтому один ADR отвечает на один связный вопрос, а не пытается стать полной технической документацией системы.

\n

В шаблоне Michael Nygard минимальная форма состоит из Status, Context, Decision и Consequences. В MADR форма расширяется полями decision drivers, considered options, outcome и confirmation. Это полезные шаблоны, а не универсальный закон: команда может добавить владельца, ссылки на контракт, confidence или условие пересмотра.

\n
Граница ответственности ADR и соседних артефактов
АртефактГлавный вопросЧто должно остаться внутриЧего он не доказывает
ADRПочему выбран этот вариант?Контекст, ограничения, альтернативы, решение и последствияЧто код уже корректен во всех средах
Design documentКак устроить решение целиком?Компоненты, потоки, контракты и детали реализацииЧто выбранный дизайн принят как долгосрочное решение
TestКакое поведение воспроизводится?Условия, входы, ожидаемый результат, регрессияПочему выбрана именно эта архитектура
RunbookЧто делать при операционном событии?Команды, роли, stop condition и восстановлениеЧто исходный компромисс всё ещё применим
\n

Ссылка на ticket или commit полезна для навигации, но не заменяет rationale. И наоборот, ADR не должен превращаться в копию implementation guide. Если решение требует детальной схемы, threat model или плана миграции, запишите выбор в ADR и свяжите его с отдельным документом.

\n

Сначала зафиксируйте симптом и границу

\n

Начинайте с наблюдаемого симптома, а не с названия технологии. Например: «клиент вызывает экспорт, но операция иногда не укладывается в границу HTTP-запроса; при повторе непонятно, создан ли второй экспорт». Это факт и вопрос. Гипотеза «нам нужна очередь» появляется только после проверки длительности операции, повторяемости запроса и требований caller.

\n

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

\n

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

\n
Проблема: export иногда дольше жизни HTTP-запроса.\nОбязательные условия:\n- клиент получает подтверждение создания операции;\n- повтор запроса не создаёт второй экспорт;\n- состояние имеет срок хранения и владельца очистки.\nЦена ошибки: дубль работы или запись, которую никто не удаляет.\nВопрос решения: где хранить status и кто отвечает за его переходы?
\n

Фрагмент — учебная заготовка. Он не сообщает реальную длительность, нагрузку или процент ошибок. В рабочей записи эти утверждения должны ссылаться на контракт, trace, тест, incident или другой доступный источник. Если данных нет, оставьте evidence gap и действие по его закрытию.

\n

Сравнивайте альтернативы одной меркой

\n

Сравнение ломается, когда выбранный вариант описан конкретно, а остальные получают ярлык «сложно». Приведите варианты к одному уровню и задайте одинаковые вопросы: выполняет ли вариант обязательные условия, где хранится состояние, как работает повтор, кто поддерживает очистку, как выглядит возврат и какое evidence уже есть.

\n
Учебное сравнение способов выполнить долгий экспорт
ВариантЧто закрываетЦена и рискПуть назад
Синхронный ответПростая модель для короткой операцииНе выдерживает временную границу; повтор может повторить работуНе требуется отдельное состояние, но граница остаётся нерешённой
Очередь и status у сервиса экспортаОтделяет подтверждение создания от завершения работыНужны idempotency key, срок хранения, cleanup и наблюдение переходовОстановить новые async-запуски и вернуть короткие операции на прямой путь
Общий workflow между сервисамиЕдиный статус для нескольких владельцев процессаПоявляется общий контракт, координатор и cross-service ownershipУдалить интеграцию нельзя одной заменой; сначала нужен план вывода потребителей
Ничего не менятьНе добавляет новый компонентСохраняет таймауты, повторы и неясную ответственностьОбратимость высокая, но исходный симптом остаётся
\n

В этой матрице нет итогового балла. Присвоить «3» за надёжность и «2» за стоимость можно только после определения шкалы, веса и источника данных. Без этого score создаёт видимость точности. Для небольшого решения достаточно назвать обязательное условие, показать trade-off и отметить, какие вопросы ещё не проверены.

\n
\"Синтетическая
Схема показывает форму сравнения, а не рейтинг реальных архитектур. Значения в иллюстрации синтетические: их нельзя трактовать как измерение production-нагрузки или стоимости.
\n

Запишите выбранный компромисс

\n

После сравнения решение должно отвечать на вопрос «что меняем и почему именно сейчас». Важно записать не только положительную сторону. Для status-модели положительный эффект — caller не удерживает один долгий запрос. Отрицательная сторона — сервис теперь владеет жизненным циклом операции, а значит, обязан определить повтор, истечение срока, очистку и поведение при падении worker.

\n
# ADR-0042: асинхронный экспорт с локальным status\n\n## Status\nAccepted\n\n## Context and Problem Statement\nЭкспорт может выйти за временную границу HTTP-запроса. Повтор\nзапроса должен быть безопасным, а запись операции — удаляемой.\n\n## Decision Drivers\n- подтверждение создания отдельно от завершения;\n- идемпотентный повтор;\n- явные expiry и owner состояния;\n- возможность ограниченного возврата.\n\n## Considered Options\n1. Синхронный ответ.\n2. Очередь и status у сервиса экспорта.\n3. Общий workflow между сервисами.\n\n## Decision Outcome\nВыбираем очередь и status у сервиса экспорта: граница владения\nостаётся локальной, а caller получает отдельный контракт состояния.\n\n## Consequences\nПлюс: долгую работу можно наблюдать и повторять безопасно.\nМинус: нужны хранение, expiry, cleanup, retry policy и метрики.\nНе делаем: не превращаем локальный status в общий workflow.\n\n## Confirmation\nПроверить контракт повторного запроса, переходы status, expiry,\nудаление результата и возврат короткой операции на direct path.
\n

Это воспроизводимый формат записи, но не готовая реализация очереди. Поля Accepted, expiry и owner не запускают процессы сами по себе. Их смысл должен быть закреплён в правилах репозитория: кто принимает запись, где она хранится, как связана с кодом и что считается подтверждением.

\n

Проверьте отрицательный путь, а не только happy path

\n

У решения есть границы «не делаем». Они защищают локальный механизм от незаметного расширения. В нашем примере сервис экспортов отвечает за собственные операции. Он не становится координатором платежей, уведомлений и нескольких downstream-сервисов. Если появился caller, которому нужен общий status процесса, это новое архитектурное условие, а не повод дописать исключение в старом ADR.

\n

Для каждой границы задайте обратный путь. Что остановит новые записи? Как найти незавершённые операции? Сколько времени хранить status? Что увидит клиент, если worker завершил файл, но не записал финальный статус? Ответы должны быть в контракте и runbook, а в ADR достаточно зафиксировать решение, цену и ссылки на эти проверки.

\n
От симптома к проверке и действию
СимптомГипотезаПроверкаДействие
Один idempotency key даёт два заданияКлюч проверяют после постановки в очередьПовторить запрос на тестовом хранилище и сравнить созданные idsПеренести дедупликацию к границе создания и добавить регрессионный тест
Status показывает done, а файл недоступенПереход статуса не связан с публикацией результатаПроверить порядок записи результата и смены status при сбоеСделать переход атомарным для контракта или описать промежуточное состояние
Старые операции растут без ограниченияExpiry записан в ADR, но нет владельца cleanupНайти job, журнал удаления и тест срока храненияНазначить owner и не считать retention выполненным без evidence
Новый сервис использует локальный statusГраница применимости не стала частью контрактаПроверить список consumers и требования к общему процессуОстановить расширение и создать successor ADR при новом scope
\n

Подтвердите связь записи с кодом

\n

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

\n
set -eu\nadr='docs/decisions/ADR-0042-export-status.md'\ntest -s \"$adr\"\ngrep -q '^## Status$' \"$adr\"\ngrep -q '^## Context and Problem Statement$' \"$adr\"\ngrep -q '^## Decision Drivers$' \"$adr\"\ngrep -q '^## Considered Options$' \"$adr\"\ngrep -q '^## Decision Outcome$' \"$adr\"\ngrep -q '^## Consequences$' \"$adr\"\ngrep -q '^## Confirmation$' \"$adr\"\nprintf '%s\\n' 'ADR structure: OK'
\n

Запустите эту проверку из корня репозитория после создания файла и замените путь на свой. Она проверяет только структуру Markdown. Она не доказывает идемпотентность, безопасность, latency, корректность миграции или наличие owner. Для этого нужны публичные тесты, review кода, нагрузочная проверка, threat model или операционный dry run — в зависимости от решения.

\n

Сохраните историю и условие пересмотра

\n

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

\n

Статусы тоже требуют локального соглашения. Proposed может означать «готово к обсуждению», Accepted — «принято», Rejected — «рассмотрено и отклонено», Superseded — «заменено новой записью». Названия встречаются в официальных шаблонах, но workflow approval, даты и права на изменение задаёт конкретная команда.

\n

Сигнал пересмотра лучше формулировать как наблюдаемое условие: появился consumer, требующий общего status; retention превысил согласованный предел; contract upstream изменился; worker не укладывается в заданный класс операций. Дата review может напомнить о проверке, но сама по себе не означает, что решение устарело. У сигнала должен быть владелец и действие, иначе это пожелание.

\n
## Reassessment\nОткрыть ADR-0043, если выполняется хотя бы одно условие:\n- второй сервис требует читать status как общий workflow;\n- изменился контракт повторного запроса;\n- retention больше согласованного срока;\n- cleanup не подтверждён тестом или операционным журналом.\nДействие: сравнить локальный status, общий workflow и прямой путь;\nстарую запись не редактировать.
\n

Рабочий порядок для команды

\n
  1. Сузьте вопрос. Назовите один значимый выбор, конкретную границу и затронутый контракт.
  2. Отделите факт от гипотезы. Запишите симптом и ссылку на источник; неподтверждённое вынесите в evidence gap.
  3. Назовите цену ошибки. Укажите, какие данные, время, права или обязанности будут потеряны при неверном пути.
  4. Соберите доступные варианты. Приведите их к одному уровню и добавьте «ничего не менять», если такой путь реален.
  5. Сравните по одинаковым drivers. Проверьте constraint fit, обратимость, операционную цену, владельца и остаточные риски.
  6. Запишите decision и отрицательный путь. Ясно укажите, что команда делает и чего намеренно не делает.
  7. Назначьте confirmation. Свяжите решение с тестом, контрактом, benchmark, threat model, runbook или другим проверяемым артефактом.
  8. Определите reassessment. Запишите наблюдаемый сигнал, владельца и действие; не полагайтесь на одну календарную дату.
  9. Сверьте ADR с реализацией. Если код выбрал другой вариант, исправьте расхождение или оформите новый decision, не переписывая историю.
\n

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

\n

ADR полезен для значимых решений: границ компонентов, API и data contracts, зависимостей, non-functional requirements, способов миграции и технологических направлений. Для тривиального локального рефакторинга отдельная запись может создать больше шума, чем контекста. Порог значимости определяется командой; важно сделать его явным.

\n

ADR не является design document, threat model, benchmark, тестовым раннером, runbook или approval-системой. Он не гарантирует, что команда нашла все альтернативы, и не превращает отсутствие данных в доказанную безопасность. Для персональных данных, security, legal и recovery нужны профильные проверки с отдельными владельцами. Не публикуйте в открытом ADR секреты, персональные данные и внутренние ссылки без разрешённого доступа.

\n

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

\n

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

\n" } diff --git a/editorial/agent-rewrites/120.json b/editorial/agent-rewrites/120.json index 04592da..d98c6e1 100644 --- a/editorial/agent-rewrites/120.json +++ b/editorial/agent-rewrites/120.json @@ -1 +1 @@ -{"index":120,"slug":"editorial-2024-09-practice-adr-decisions","title":"ADR без бюрократии: как сохранить причину технического решения","excerpt":"Практический разбор ADR: от наблюдаемого симптома и цены ошибки до проверяемого решения, отрицательного пути и даты пересмотра.","contentHtml":"

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

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

Тезис: ADR хранит причину, а не оправдание

Architecture Decision Record фиксирует один значимый выбор. Он отвечает на пять вопросов: что произошло, какие ограничения действовали, какие варианты сравнили, что выбрали и какую цену приняли. Отдельно записывают владельца, статус и сигнал для пересмотра. ADR не объявляет решение вечным и не доказывает, что реализация корректна.

Это важная граница. Ticket хранит работу и сроки. Code review хранит обсуждение изменения. Test проверяет поведение. Runbook описывает операционное действие. ADR хранит rationale — причину, по которой команда выбрала один путь среди допустимых. Ссылка на ticket не заменяет rationale, а commit не заменяет сравнение альтернатив.

Механизм: от симптома к записи

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

Затем ограничьте вопрос. «Как устроить export» слишком широко. «Где caller получает status, если synchronous boundary не выполняется» уже задаёт предмет. Один ADR должен описывать один выбор. Если обсуждение меняет два независимых контракта, разделите записи и свяжите их ссылками.

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

Title: bounded cache at the BFF boundary\nStatus: Proposed\nOwner: application owner\nReview by: 2025-03-31\n\nContext: source contract permits bounded freshness.\nOptions: direct read; local cache; shared cache.\nDecision: choose local cache with named expiry.\nConsequences: add freshness check and direct-read rollback.\nEvidence question: does the source contract still permit this cache?

Пример учебный. Он не создаёт cache, не обращается к repository и не сообщает latency, hit ratio или экономию. Его задача — показать форму записи. В настоящем ADR каждое утверждение о контракте должно ссылаться на доступный артефакт, а владелец должен иметь право проверить его.

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

Как переводить наблюдаемую проблему в следующий шаг
СимптомПричинаПроверкаДействие
В review спорят о старом обходном путиКонтекст остался в перепискеНайдите исходное ограничение и отделите факт от гипотезыСоздайте proposed ADR и укажите ссылку на code
В записи перечислен только выбранный путьАльтернативы не стали частью решенияПроверьте, можно ли сравнить варианты по одним критериямДобавьте реально рассмотренные варианты и причины отказа
Текст обещает «простую поддержку»Последствия описаны абстрактноСпросите, кто что должен сделать и какой риск остаётсяЗапишите владельца, cost, rollback и signal пересмотра
ADR принят, но code изменился иначеЗапись смешана с реализациейСопоставьте decision с контрактом и diff отдельной проверкойИсправьте code или обновите ADR новым решением; не переписывайте историю
Ограничение больше не действуетУ записи нет review boundaryПроверьте дату, source contract и сигнал измененияСоздайте successor ADR со статусом superseded для старого

Как читать короткий ADR

Хорошая запись начинается с контекста, но не превращается в историю всей команды. Достаточно назвать границу системы, затронутый контракт, decision drivers и цену неверного выбора. Слова «быстрее», «надёжнее» и «проще» требуют уточнения. Быстрее для какого сценария? Надёжнее при каком отказе? Проще для какого владельца?

Последствия должны включать отрицательную сторону. Если выбран local cache, положительный эффект может быть ограничен целевым read path. Цена — необходимость хранить expiry, проверять freshness и иметь путь к direct read. Если данные могут быть чувствительными, добавляется отдельная проверка класса данных. Не прячьте эту цену под словом «trade-off»: читателю нужно понимать, что именно он будет поддерживать.

Запишите две границы: что решение делает и чего оно не делает. В учебном примере мы создаём bounded local state с именованным expiry. Мы не создаём shared invalidation system и не объявляем число запросов измеренным результатом. Такая отрицательная часть защищает от незаметного расширения scope.

\"Схема
Схема показывает порядок работы с решением. Она не описывает конкретную approval-систему, репозиторий, CI или состояние production.

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

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

Свяжите ADR с отдельным evidence question. Для cache это вопрос о допустимой freshness и о том, как обнаружить нарушение. Для миграции это вопрос о совместимости схемы и обратном пути. Для security-решения это вопрос о threat model и обязательной проверке. Не подменяйте evidence красивой формулировкой в разделе Consequences.

Порядок работы с одной перепиской

  1. Ограничьте вопрос. Сформулируйте один выбор, его границу и затронутый контракт.
  2. Соберите факты. Запишите наблюдаемый симптом, дату, источник, owner и цену неверного выбора. Догадки пометьте как гипотезы.
  3. Назовите варианты. Добавьте два-три реально доступных пути, включая вариант ничего не менять, если он был возможен.
  4. Сравните одинаково. Используйте один набор критериев: constraint fit, обратимость, стоимость эксплуатации и пробел в evidence.
  5. Зафиксируйте решение. Укажите выбранный вариант, status, дату и человека, который принимает решение.
  6. Опишите последствия. Назовите положительную сторону, цену, отрицательный путь и условие rollback. Не обещайте измерения, которых ещё нет.
  7. Назначьте пересмотр. Запишите дату или сигнал: изменение контракта, превышение лимита, новый класс данных или невозможность выполнить проверку.
  8. Свяжите артефакты. После реализации добавьте ссылки на code, test, metric question или runbook. Ссылки помогают навигации, но не заменяют чтение этих артефактов.
  9. Проверьте расхождение. Сравните ADR с реализацией. Если code ушёл в другой вариант, исправьте code либо оформите новый decision.

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

ADR не заменяет design document, threat model, migration plan, benchmark, test plan, runbook или incident review. Он не гарантирует полноту альтернатив и не превращает согласие участников в технический факт. Формат нужно подстроить под локальные правила хранения, доступа и approval.

Не редактируйте старую запись так, чтобы она описывала новое решение. История нужна именно для ответа на вопрос «почему раньше сделали так». Если предпосылка исчезла, старый ADR получает статус superseded, а новый record объясняет следующий компромисс. Это сохраняет причинность и не заставляет будущего читателя угадывать, какая версия текста была действующей.

Не называйте synthetic пример production-результатом. Не добавляйте вымышленные числа, названия сервисов и ссылки на несуществующие dashboards. Если факт нельзя проверить, напишите, какой артефакт должен его подтвердить. Такой пробел полезнее уверенного, но ложного вывода.

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

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

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

"} +{"index":120,"slug":"editorial-2024-09-practice-adr-decisions","title":"ADR без бюрократии: как сохранить причину технического решения","excerpt":"Практический разбор ADR на учебном BFF-кейсе: как отделить факт от гипотезы, сравнить альтернативы, записать цену выбора и понять, когда нужен новый decision record.","contentHtml":"

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

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

Такую запись обычно называют ADR (Architecture Decision Record). В этой статье разберём практический маршрут для одного решения на границе BFF (Backend for Frontend): как понять, нужен ли кэш, какие варианты сравнить и как записать результат так, чтобы он не стал ложным разрешением на любую будущую оптимизацию.

Что именно сохраняет ADR

ADR фиксирует один значимый технический выбор и его rationale — объяснение, почему выбран этот путь. Минимальное содержимое: контекст и проблема, рассмотренные варианты, decision и последствия. Для каждого утверждения полезно оставить источник: контракт, тест, измерение, issue или ссылку на другой документ.

У записи есть границы ответственности. Ticket описывает работу и срок. Code review хранит обсуждение конкретного изменения. Test проверяет поведение. Runbook описывает операционное действие. ADR отвечает на другой вопрос: почему команда выбрала одну допустимую форму системы вместо других. Ссылка на ticket помогает найти детали, но не заменяет rationale.

Статус тоже не является переключателем. Proposed означает, что запись подготовлена к обсуждению. Accepted означает, что команда приняла решение. Superseded означает, что новый ADR заменил старый и объяснил изменение. Ни один статус сам по себе не запускает код, миграцию, approval или проверку отката.

Учебный симптом на границе BFF

Возьмём ограниченный пример. SSR-приложение обращается через BFF к каталогу предложений. Для одного экрана одинаковый справочник читается много раз за короткий интервал. Прямой запрос проще и всегда получает актуальный ответ, но увеличивает число обращений к upstream. Локальный кэш может сократить повторные чтения, однако добавляет TTL, риск устаревшего ответа и обязанность очищать состояние.

Это не отчёт о конкретной production-системе: в статье нет реальных latency, traffic, hit ratio или экономии. Сценарий нужен, чтобы воспроизвести форму анализа. В своём проекте подставьте контракт источника, допустимую свежесть, класс данных, owner и измерение. Если эти сведения неизвестны, ADR должен назвать их открытым вопросом, а не превращать предположение в факт.

Сначала запишем наблюдаемое и неизвестное отдельно:

Наблюдаемый факт: один экран повторяет чтение одного справочника.\nИсточник факта: trace или счётчик запросов, приложенный к задаче.\nГипотеза: часть чтений можно обслужить из bounded local cache.\nОбязательное условие: ответ не старше согласованного TTL.\nНеизвестно: разрешает ли upstream такой уровень устаревания.\nЦена ошибки: устаревшие данные или рост нагрузки на upstream.

Важна именно последовательность. «Добавим кэш, потому что медленно» — решение без доказанной причины. Сначала нужно подтвердить повторяемость чтений и спросить владельца контракта, допустима ли заданная свежесть. Если upstream требует read-after-write для этого справочника, локальный кэш не проходит constraint fit независимо от удобства реализации.

Как сравнивать варианты на одной шкале

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

Учебная матрица для решения о кэше на BFF-границе
ВариантЧто закрываетЦена и рискЧто проверить
Прямой запросАктуальность ответа без локального состоянияПовторная нагрузка на upstream; нет TTL и cleanupЛимит запросов, latency и допустимость текущей нагрузки
Bounded local cacheПовторное чтение в пределах объявленного TTLУстаревший ответ, invalidation, память и владелец кэшаКонтракт freshness, класс данных, hit ratio и путь direct read
Общий cache-сервисРазделяемое состояние для нескольких потребителейНовый сервис, сеть, права, отказ и cross-team ownerДействительно ли нужен общий владелец и как переживается недоступность
Ничего не менятьСохраняет простую модель и отсутствие нового состоянияНагрузка остаётся; возможно, проблема не подтвержденаПовторить измерение и проверить, есть ли пользовательский эффект

Матрица не выдаёт победителя автоматически. Она заставляет привести варианты к сопоставимому масштабу. Если для кэша известен TTL, а для общего сервиса написано только «сложно», данные ещё не симметричны. Если используете баллы и веса, приложите шкалу и объясните, какие риски модель не учитывает. Итоговый score помогает провести разговор, но не доказывает корректность решения.

\"Поток
ADR связывает контекст и выбор, но реализация и evidence проходят отдельную проверку. Ветвь successor ADR сохраняет историю, если исходное условие изменилось.

Минимальная запись решения

После проверки контракта допустим учебный исход: upstream разрешает ограниченную свежесть, справочник не содержит данных, требующих немедленного read-after-write, а direct read остаётся доступным при промахе или отключении кэша. Тогда можно предложить локальное bounded state. Ниже — запись в формате, который можно положить в каталог decisions. Она не создаёт кэш и не утверждает измеренный результат.

# ADR-0042: bounded cache at the BFF boundary\n\n## Status\nProposed\n\n## Context and problem\nSSR повторяет чтение одного справочника. Upstream разрешает\nответ не старше согласованного TTL. Для критичных изменений\nтребуется direct read.\n\n## Options considered\n- direct read;\n- bounded local cache with explicit TTL;\n- shared cache service.\n\n## Decision\nНачать с bounded local cache только для этого read path.\nПри недоступном кэше читать upstream напрямую.\n\n## Consequences\nПлюс: повторные чтения ограничены TTL.\nЦена: owner состояния, expiry, наблюдение за freshness и cleanup.\n\n## Not in scope\nНе вводим shared cache-сервис и не распространяем решение\nна другие callers без нового ADR.\n\n## Reassessment\nОткрыть successor ADR при изменении контракта freshness,\nпоявлении чувствительных данных или нового потребителя.

Учебный фрагмент показывает причинность: симптом связан с ограничением, варианты перечислены, цена названа, а отрицательный путь ограничивает scope. В настоящую запись добавьте ссылки на подтверждённый контракт, тест, trace или controlled experiment. Статус Proposed означает, что документ ещё нужно проверить владельцу upstream и участникам, которых затрагивает кэш.

Как связать ADR с реализацией и evidence

Решение и доказательство нельзя слить в один абзац. После принятия ADR команда отдельно проверяет реализацию: TTL действительно ограничен, ключ кэша не смешивает разные контексты, промах или ошибка не блокируют direct read, а чувствительные данные не попадают в неподходящее хранилище. Это уже область тестов, threat model, ревью и наблюдаемости.

Полезно сформулировать evidence question до изменения кода. Например: «Для выбранного read path доля повторных запросов превышает порог X, а upstream разрешает TTL Y секунд». Числа X и Y должны прийти из вашего контракта и измерения; в учебной статье их нельзя выдумывать. По итогам controlled test в ADR можно добавить ссылку на артефакт и уточнить последствия. Сам факт наличия ссылки не превращает эксперимент в гарантию для всех нагрузок.

Связь с кодом должна быть двусторонней и короткой: ADR ссылается на реализацию, тест и метрику; code review ссылается на ADR, чтобы reviewer мог проверить границу решения. Если diff добавляет shared invalidation, новую схему данных или другой срок хранения, это уже не «деталь», а возможное новое решение. Не прячьте его в implementation note.

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

Предпосылки решения со временем меняются. Upstream может начать требовать строгую свежесть, появится второй потребитель, изменится класс данных или стоимость shared service станет приемлемой. В такой ситуации старый ADR не следует переписывать задним числом. Иначе исчезнет ответ на вопрос, почему прежний код был разумным при старых условиях.

Создайте новый record со статусом Proposed, свяжите его с исходным и опишите, что именно изменилось. После принятия нового решения старое получает Superseded. Это не формальность: append-only история отделяет прежний компромисс от нового и помогает безопасно читать старые ссылки в issue, review и runbook.

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

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

Типовые ошибки при создании ADR
СимптомВероятная причинаПроверкаСледующее действие
Записан только победительРешение приняли по вкусу, а не по constraintЗаполнить одинаковые поля для двух-трёх вариантовДобавить downside выбранного пути и причины отказа
В Consequences обещана экономияУчебную гипотезу выдали за измерениеНайти trace, benchmark или метрику с единицей измеренияУбрать число либо добавить ссылку на evidence
Кэш расширяется на новых callersГраница применения осталась неявнойСверить список потребителей и contract freshnessОстановить расширение и открыть новый ADR
Флаг выключили, но старый путь удалилиRollback смешали с cleanupПроверить, может ли система вернуться к direct readРазделить изменение поведения и удаление ветки
Новый ADR переписывает старыйИсторию принимают за текущую конфигурациюПроверить ссылки и статусы двух записейОставить старый текст, связать successor и объяснить drift

Эта таблица применима как диагностическая последовательность, а не как чек-лист полноты архитектуры. Например, ошибка кэша может быть не в выборе варианта, а в неверном ключе или cache-control заголовке. ADR помогает увидеть исходный компромисс, но не заменяет проверку конкретной реализации.

Практический порядок работы

  1. Сузьте вопрос. Назовите одного caller, одну границу и одно обязательное условие. «Ускорить систему» слишком широко; «ограничить повторные чтения этого справочника при допустимой свежести» проверяемо.
  2. Соберите факты. Укажите наблюдаемый симптом, источник, дату наблюдения и цену неверного выбора. Отделите гипотезу от того, что действительно видно в trace, контракте или тесте.
  3. Проверьте constraint. Спросите владельца upstream о freshness, лимитах, правах и классе данных. Неподтверждённое условие оставьте evidence gap.
  4. Перечислите варианты. Добавьте два-три пути одного масштаба. Включите «ничего не менять», если он не нарушает обязательное условие.
  5. Сравните одинаково. Для каждого варианта назовите границу, owner, обратимость, операционную цену, неизвестное и отрицательный путь.
  6. Запишите decision. Формулировка должна сказать, что делаем и чего не делаем. Не объявляйте учебный пример результатом production.
  7. Проведите review. Участники должны проверить context, alternatives, consequences, status и затронутые контракты до начала реализации.
  8. Свяжите evidence. После controlled test или релиза добавьте ссылки на код, тест, trace, метрику или runbook. Ссылка не заменяет саму проверку.
  9. Проверьте drift. При новом consumer, изменении контракта или другой цене решения откройте successor ADR, а не дописывайте старый задним числом.

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

ADR не заменяет design document, threat model, migration plan, benchmark, test plan, runbook или incident review. Он не измеряет latency, надёжность, стоимость и безопасность без соответствующего evidence. Он также не гарантирует, что команда нашла все варианты: качество зависит от состава участников, доступных фактов и времени на review.

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

Локальный кэш из примера нельзя переносить на персональные или чувствительные данные без отдельного анализа хранения, доступа и очистки. BFF не становится владельцем бизнес-истины только потому, что временно хранит ответ. При строгой консистентности direct read или иной контракт может быть единственно допустимым вариантом.

Форматы ADR различаются. Можно хранить записи в Git, wiki или специальном каталоге, если команда сохраняет версионирование, доступность, единый шаблон и историю статусов. Конкретные поля вроде confidence, stakeholders и review trigger выбираются по риску решения. Источник формата не является разрешением игнорировать требования безопасности, права доступа и отраслевые процессы.

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

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

Финальная проверка короткая. Уберите из записи заголовок Decision и попросите коллегу восстановить выбор по Context, Options и Consequences. Если коллега не может назвать owner, отрицательный путь или сигнал пересмотра, ADR ещё не готов. Если запись обещает измеренный эффект без источника, эффект нужно убрать или измерить отдельно.

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

"} diff --git a/editorial/agent-rewrites/121.json b/editorial/agent-rewrites/121.json index 1bdb86c..f21b286 100644 --- a/editorial/agent-rewrites/121.json +++ b/editorial/agent-rewrites/121.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-08-field-feature-flags", "title": "Feature flag после rollout: как отделить rollback от cleanup", "excerpt": "Выключенный feature flag прекращает выдачу нового поведения, но не удаляет ветки, конфигурацию и несовместимые данные. Разбираем безопасный rollout, проверяемый rollback и отдельный cleanup gate.", - "contentHtml": "

Новый экран работает у сотрудников, но часть клиентов внезапно видит его после релиза. Команда ставит feature flag в false. Ошибка исчезает, график успокаивается, change закрывают. Через месяц в коде всё ещё живут две ветки, в конфигурации лежит старый ключ, а тесты проверяют два ответа. Любая случайная смена окружения может вернуть candidate-путь. Цена ошибки — повторный инцидент, долгий поиск владельца и риск для данных, которые новый путь уже успел записать.

\n

Обратный случай не безопаснее. Rollout достиг 100 процентов, и это объявили завершением. Но 100 процентов означает только текущий результат правила для объявленной аудитории. Это не доказывает совместимость данных, исправность recovery path и право удалить fallback. Feature flag — не одно переключение, а временный контракт: кто получает вариант, кто его вычисляет, что считается остановкой и когда старый путь перестаёт быть нужен.

\n

Тезис. Rollout, rollback и cleanup нужно проводить разными изменениями. Rollout расширяет аудиторию. Rollback останавливает candidate и возвращает fallback. Cleanup удаляет лишнюю ветку после выбора итогового поведения. Если смешать эти операции, команда теряет причинную связь и принимает отключение за исправление.

\n

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

\n
СимптомПричинаПроверкаДействие
Флаг выключен, но в коде остались две веткиRollback смешали с удалением реализацииНайти evaluator, branches, config references и тесты по ключуОставить fallback исполняемым, открыть отдельный cleanup change
100% rollout считают доказательством готовностиПроцент подменяет проверку контракта и данныхСверить cohort key, effective config version, stop condition и recovery pathЗаморозить процент и провести решение о final variant
После возврата к fallback часть запросов получает ошибкуCandidate записал состояние, которое fallback не читаетПроверить read/write compatibility и миграционный контрактОстановить расширение; исправить совместимость отдельно от flag value
После удаления ключ снова появляется в UIОстался client check, stale cache или старый payloadПроверить server response, client bundle, cache key и configУдалить presentation toggle и оставить только итоговый UI contract
Никто не может назвать дату удаленияУ флага нет owner, final variant и cleanup gateЗапросить карточку решения с evidence и списком referencesНе расширять rollout; назначить владельца и новый change
\n

Как работает решение

\n

Сначала evaluator получает ключ флага и контекст. Контекст должен однозначно описывать subject, окружение и другие поля, которые участвуют в targeting. Для процентного rollout особенно важен стабильный cohort key. Если сегодня используется user id, а завтра случайный request id, один пользователь будет переходить между вариантами. Это уже не gradual rollout, а непредсказуемое распределение.

\n

Evaluator возвращает выбранный вариант и, если система это поддерживает, детали оценки: key, value, variant, reason и версию конфигурации. Приложение применяет результат в одной точке. Второй слой не должен пересчитывать то же правило с другим контекстом. Иначе сервер отдаст новый ответ, а клиент решит, что пользователь относится к fallback-группе.

\n

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

\n
type FlagDecision = {\n  key: string;\n  variant: 'fallback' | 'candidate';\n  cohortKey: string;\n  configVersion: string;\n};\n\nfunction renderCheckout(decision: FlagDecision, order: Order) {\n  if (decision.variant === 'candidate') {\n    return renderNewCheckout(order);\n  }\n\n  return renderLegacyCheckout(order);\n}
\n

Этот фрагмент — учебный. Он показывает границу между результатом оценки и применением варианта. В нём нет настоящего flag provider, хранилища, телеметрии, миграции или команды rollback. В рабочей системе нужно дополнительно определить формат контекста, источник версии конфигурации, правила кеширования и поведение при ошибке evaluator.

\n

Учебная лестница rollout

\n
ЭтапВопросСтоп-условиеДействие
0%Fallback выполняет согласованный smoke scenario?Старый путь уже не работаетНе включать candidate; исправить базовый контракт
5%Одна стабильная cohort получает candidate в объявленной версии?Сигнал нарушает заранее записанную границуВернуть аудиторию к 0%, сохранить конфигурацию и вопрос расследования
25%Candidate можно сопоставить с fallback в одном окне?Версия или источник сигнала неизвестныОстановить расширение и не менять правило вместе с кодом
100%Вся объявленная аудитория получает выбранный вариант?Final variant не утверждён или fallback нужен для recoveryНе удалять флаг; открыть решение о завершении
Cleanup gateКод и конфигурация больше не требуют alternate branch?Осталась runtime reference или зависимость данныхВернуть cleanup в доработку
\n

Значения 0, 5, 25 и 100 процентов в таблице — фиксированный учебный пример. Они не являются рекомендацией, нормативом или результатом измерения. Реальный шаг выбирают по размеру риска, качеству сигнала, обратимости и размеру cohort. Если команда не может назвать population, control, окно и владельца решения, процент не даёт полезной информации.

\n
\"Rollout
Rollout расширяет аудиторию, rollback возвращает fallback, cleanup удаляет alternate branch. Проценты на схеме относятся к учебной модели.
\n

Rollback возвращает поведение, но не переписывает историю

\n

При stop condition минимальное действие — прекратить дальнейшее расширение candidate. Для этого возвращают аудиторию к fallback или к заранее определённому safe value. Записывают effective config version и причину остановки. Не нужно в тот же момент удалять ветки, менять evaluator и переписывать миграцию. Иначе нельзя будет понять, что именно остановило симптом.

\n

Rollback значения флага не равен rollback данных. Если candidate уже создал записи, отправил события или изменил схему ответа, старый путь должен уметь прочитать это состояние. Фича-флаг не добавляет backward compatibility автоматически. Если fallback не понимает результат candidate, ответом должен стать отдельный migration contract, а не уверенность, что false решает всё.

\n

Также не стоит называть rollback мгновенным. Сервис, edge-кеш и браузер могут получить конфигурацию в разное время. В карточке изменения укажите, где вычисляется вариант, сколько живёт кеш и что происходит с активной сессией. Разница версий допустима только тогда, когда она входит в контракт и не ломает recovery path.

\n

Cleanup — отдельное решение

\n

Cleanup начинается после выбора единственного поведения. Сначала владелец фиксирует final variant. Затем команда перестаёт добавлять новые условия и открывает отдельное изменение удаления. В его области должны быть server branches, client checks, configuration, permissions, tests, документация и миграционные заметки.

\n

Проверка cleanup строится на отрицательных утверждениях. Runtime больше не вызывает evaluator для ключа. Клиент не переключает UI по старому payload. Конфигурация не содержит targeting rules и environment overrides, нужные только флагу. Тесты проверяют итоговый business contract, а не сохранение двух boolean-веток. Документ объясняет принятое поведение, но не предлагает включить исчезнувший путь.

\n

Глобальный поиск по имени ключа полезен, но недостаточен. Ключ может быть собран из частей, сохранён в типе, скрыт в конфиге или пришит к кешу. Ищите вызовы evaluator, schema fields, token scopes, generated client types и тестовые данные. Если reference нужна для обратимой миграции, cleanup ещё не завершён: у неё должен быть owner и срок следующей проверки.

\n

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

\n
  1. Опишите контракт. Назовите owner, evaluator, cohort key, fallback, config version и данные, которые меняет candidate.
  2. Проверьте fallback. Выполните smoke scenario и убедитесь, что старый путь доступен до включения нового.
  3. Запишите stop condition. Свяжите её с конкретным сигналом, окном, источником и действием на остановке.
  4. Запускайте один этап. Не меняйте одновременно аудиторию, правило, код и схему данных.
  5. При проблеме остановите расширение. Верните cohort к fallback, сохраните effective config version и отделите mitigation от поиска причины.
  6. Проверьте совместимость данных. Убедитесь, что fallback читает состояние, которое оставил candidate, либо оформите отдельную миграцию.
  7. После выбора final variant заморозьте flag policy. Не добавляйте новые условия и не продлевайте флаг без новой причины.
  8. Проведите cleanup search. Проверьте server, client, config, кеши, права, тесты и документацию.
  9. Удалите артефакты отдельным change. Сохраните тест итогового поведения и обычный rollback plan для самого cleanup-релиза.
  10. Закройте решение. Зафиксируйте final variant, owner, причину удаления и evidence отсутствия runtime-зависимости.
\n

Ограничения

\n

Этот подход не выбирает конкретный SDK, размер cohort, TTL кеша, SLO или схему миграции. OpenFeature даёт общий API для evaluation context и результата, но не задаёт правила вашей платформы. Kubernetes rollback относится к версии Deployment и его Pod template; он не доказывает обратимость бизнес-данных. Система флагов также не доказывает качество эксперимента: для этого нужны отдельные метрики, контрольная группа и методика измерения.

\n

Не следует удалять fallback только потому, что candidate получил 100 процентов. Не следует сохранять flag навсегда только потому, что когда-то существовал инцидент. Если данные, кеш или долгоживущий consumer не описаны, это блокер cleanup. Если evaluator недоступен, нужен явный default и проверяемая политика ошибки. Если неизвестно, какой вариант является итоговым, сначала принимают это решение, а потом удаляют код.

\n

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

\n

Cleanup готов, если независимая проверка отвечает «да» на все вопросы: final variant записан владельцем; fallback больше не нужен для recovery или миграции; runtime не зависит от ключа; client и server не пересчитывают старое правило; configuration и permissions не содержат флаговые остатки; тесты сохраняют итоговое поведение; обычный релизный rollback не требует возвращать удалённый flag.

\n

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

\n

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

\n

OpenFeature: Evaluation Context — официальный материал о контексте оценки, targeting key и рисках передачи персональных данных.

\n

OpenFeature: Evaluation API — официальное описание результата оценки и его деталей.

\n

Kubernetes: Update a Deployment Without Downtime — официальная документация о проверке обновления и возврате Deployment к предыдущей ревизии.

" + "contentHtml": "

Новый экран работает у сотрудников, но часть клиентов внезапно видит его после релиза. Команда ставит feature flag в false. Ошибка исчезает, график успокаивается, change закрывают. Через месяц в коде всё ещё живут две ветки, в конфигурации лежит старый ключ, а тесты проверяют два ответа. Любая случайная смена окружения может вернуть candidate-путь. Цена ошибки — повторный инцидент, долгий поиск владельца и риск для данных, которые новый путь уже успел записать.

\n

Обратный случай не безопаснее. Rollout достиг 100 процентов, и это объявили завершением. Но 100 процентов означает только текущий результат правила для объявленной аудитории. Это не доказывает совместимость данных, исправность recovery path и право удалить fallback. Feature flag — не одно переключение, а временный контракт: кто получает вариант, кто его вычисляет, что считается остановкой и когда старый путь перестаёт быть нужен.

\n

Тезис. Rollout, rollback и cleanup нужно проводить разными изменениями. Rollout расширяет аудиторию. Rollback останавливает candidate и возвращает fallback. Cleanup удаляет лишнюю ветку после выбора итогового поведения. Если смешать эти операции, команда теряет причинную связь и принимает отключение за исправление.

\n

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

\n
СимптомПричинаПроверкаДействие
Флаг выключен, но в коде остались две веткиRollback смешали с удалением реализацииНайти evaluator, branches, config references и тесты по ключуОставить fallback исполняемым, открыть отдельный cleanup change
100% rollout считают доказательством готовностиПроцент подменяет проверку контракта и данныхСверить cohort key, effective config version, stop condition и recovery pathЗаморозить процент и провести решение о final variant
После возврата к fallback часть запросов получает ошибкуCandidate записал состояние, которое fallback не читаетПроверить read/write compatibility и миграционный контрактОстановить расширение; исправить совместимость отдельно от flag value
После удаления ключ снова появляется в UIОстался client check, stale cache или старый payloadПроверить server response, client bundle, cache key и configУдалить presentation toggle и оставить только итоговый UI contract
Никто не может назвать дату удаленияУ флага нет owner, final variant и cleanup gateЗапросить карточку решения с evidence и списком referencesНе расширять rollout; назначить владельца и новый change
\n

Как работает решение

\n

Сначала evaluator получает ключ флага и контекст. Контекст должен однозначно описывать subject, окружение и другие поля, которые участвуют в targeting. Для процентного rollout особенно важен стабильный cohort key. Если сегодня используется user id, а завтра случайный request id, один пользователь будет переходить между вариантами. Это уже не gradual rollout, а непредсказуемое распределение.

\n

Evaluator возвращает выбранный вариант и, если система это поддерживает, детали оценки: key, value, variant, reason и версию конфигурации. Приложение применяет результат в одной точке. Второй слой не должен пересчитывать то же правило с другим контекстом. Иначе сервер отдаст новый ответ, а клиент решит, что пользователь относится к fallback-группе.

\n

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

\n
type FlagDecision = {\n  key: string;\n  variant: 'fallback' | 'candidate';\n  cohortKey: string;\n  configVersion: string;\n};\n\nfunction renderCheckout(decision: FlagDecision, order: Order) {\n  if (decision.variant === 'candidate') {\n    return renderNewCheckout(order);\n  }\n\n  return renderLegacyCheckout(order);\n}
\n

Этот фрагмент — учебный. Он показывает границу между результатом оценки и применением варианта. В нём нет настоящего flag provider, хранилища, телеметрии, миграции или команды rollback. В рабочей системе нужно дополнительно определить формат контекста, источник версии конфигурации, правила кеширования и поведение при ошибке evaluator.

\n

Команды для проверки ревизии

\n

Если candidate доставляется отдельным Kubernetes Deployment, сначала проверьте историю и состояние rollout. Эти команды требуют настроенного кластера, namespace и права чтения. Значение 7 — пример ревизии: его нужно заменить результатом первой команды.

\n
kubectl rollout history deployment/checkout -n shop\nkubectl rollout status deployment/checkout -n shop --timeout=60s\n\n# Изменение состояния: выполняйте только по утверждённому stop condition\nkubectl rollout undo deployment/checkout -n shop --to-revision=7\nkubectl rollout status deployment/checkout -n shop --timeout=60s
\n

rollout undo меняет состояние Deployment, поэтому это операционная команда, а не безопасная проверка «на всякий случай». Kubernetes откатывает Pod template к выбранной ревизии; он не возвращает записи в базе, события и внешние побочные эффекты. Если команда относится только к конфигурации feature flag, применяйте аналогичный runbook вашего провайдера и сохраняйте effective version.

\n

Учебная лестница rollout

\n
ЭтапВопросСтоп-условиеДействие
0%Fallback выполняет согласованный smoke scenario?Старый путь уже не работаетНе включать candidate; исправить базовый контракт
5%Одна стабильная cohort получает candidate в объявленной версии?Сигнал нарушает заранее записанную границуВернуть аудиторию к 0%, сохранить конфигурацию и вопрос расследования
25%Candidate можно сопоставить с fallback в одном окне?Версия или источник сигнала неизвестныОстановить расширение и не менять правило вместе с кодом
100%Вся объявленная аудитория получает выбранный вариант?Final variant не утверждён или fallback нужен для recoveryНе удалять флаг; открыть решение о завершении
Cleanup gateКод и конфигурация больше не требуют alternate branch?Осталась runtime reference или зависимость данныхВернуть cleanup в доработку
\n

Значения 0, 5, 25 и 100 процентов в таблице — фиксированный учебный пример. Они не являются рекомендацией, нормативом или результатом измерения. Реальный шаг выбирают по размеру риска, качеству сигнала, обратимости и размеру cohort. Если команда не может назвать population, control, окно и владельца решения, процент не даёт полезной информации.

\n
\"Rollout
Rollout расширяет аудиторию, rollback возвращает fallback, cleanup удаляет alternate branch. Проценты на схеме относятся к учебной модели.
\n

Rollback возвращает поведение, но не переписывает историю

\n

При stop condition минимальное действие — прекратить дальнейшее расширение candidate. Для этого возвращают аудиторию к fallback или к заранее определённому safe value. Записывают effective config version и причину остановки. Не нужно в тот же момент удалять ветки, менять evaluator и переписывать миграцию. Иначе нельзя будет понять, что именно остановило симптом.

\n

Rollback значения флага не равен rollback данных. Если candidate уже создал записи, отправил события или изменил схему ответа, старый путь должен уметь прочитать это состояние. Фича-флаг не добавляет backward compatibility автоматически. Если fallback не понимает результат candidate, ответом должен стать отдельный migration contract, а не уверенность, что false решает всё.

\n

Также не стоит называть rollback мгновенным. Сервис, edge-кеш и браузер могут получить конфигурацию в разное время. В карточке изменения укажите, где вычисляется вариант, сколько живёт кеш и что происходит с активной сессией. Разница версий допустима только тогда, когда она входит в контракт и не ломает recovery path.

\n

Cleanup — отдельное решение

\n

Cleanup начинается после выбора единственного поведения. Сначала владелец фиксирует final variant. Затем команда перестаёт добавлять новые условия и открывает отдельное изменение удаления. В его области должны быть server branches, client checks, configuration, permissions, tests, документация и миграционные заметки.

\n

Проверка cleanup строится на отрицательных утверждениях. Runtime больше не вызывает evaluator для ключа. Клиент не переключает UI по старому payload. Конфигурация не содержит targeting rules и environment overrides, нужные только флагу. Тесты проверяют итоговый business contract, а не сохранение двух boolean-веток. Документ объясняет принятое поведение, но не предлагает включить исчезнувший путь.

\n

Глобальный поиск по имени ключа полезен, но недостаточен. Ключ может быть собран из частей, сохранён в типе, скрыт в конфиге или пришит к кешу. Ищите вызовы evaluator, schema fields, token scopes, generated client types и тестовые данные. Если reference нужна для обратимой миграции, cleanup ещё не завершён: у неё должен быть owner и срок следующей проверки.

\n

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

\n
  1. Опишите контракт. Назовите owner, evaluator, cohort key, fallback, config version и данные, которые меняет candidate.
  2. Проверьте fallback. Выполните smoke scenario и убедитесь, что старый путь доступен до включения нового.
  3. Запишите stop condition. Свяжите её с конкретным сигналом, окном, источником и действием на остановке.
  4. Запускайте один этап. Не меняйте одновременно аудиторию, правило, код и схему данных.
  5. При проблеме остановите расширение. Верните cohort к fallback, сохраните effective config version и отделите mitigation от поиска причины.
  6. Проверьте совместимость данных. Убедитесь, что fallback читает состояние, которое оставил candidate, либо оформите отдельную миграцию.
  7. После выбора final variant заморозьте flag policy. Не добавляйте новые условия и не продлевайте флаг без новой причины.
  8. Проведите cleanup search. Проверьте server, client, config, кеши, права, тесты и документацию.
  9. Удалите артефакты отдельным change. Сохраните тест итогового поведения и обычный rollback plan для самого cleanup-релиза.
  10. Закройте решение. Зафиксируйте final variant, owner, причину удаления и evidence отсутствия runtime-зависимости.
\n

Ограничения

\n

Этот подход не выбирает конкретный SDK, размер cohort, TTL кеша, SLO или схему миграции. OpenFeature даёт общий API для evaluation context и результата, но не задаёт правила вашей платформы; при ошибке базовой оценки клиент возвращает переданное default value. Kubernetes rollback относится к версии Deployment и его Pod template; он не доказывает обратимость бизнес-данных. Система флагов также не доказывает качество эксперимента: для этого нужны отдельные метрики, контрольная группа и методика измерения.

\n

Не следует удалять fallback только потому, что candidate получил 100 процентов. Не следует сохранять flag навсегда только потому, что когда-то существовал инцидент. Если данные, кеш или долгоживущий consumer не описаны, это блокер cleanup. Если evaluator недоступен, нужен явный default и проверяемая политика ошибки. Если неизвестно, какой вариант является итоговым, сначала принимают это решение, а потом удаляют код.

\n

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

\n

Cleanup готов, если независимая проверка отвечает «да» на все вопросы: final variant записан владельцем; fallback больше не нужен для recovery или миграции; runtime не зависит от ключа; client и server не пересчитывают старое правило; configuration и permissions не содержат флаговые остатки; тесты сохраняют итоговое поведение; обычный релизный rollback не требует возвращать удалённый flag.

\n

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

\n

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

\n

OpenFeature: Evaluation Context — официальный материал о контексте оценки, targeting key и рисках передачи персональных данных.

\n

OpenFeature: Evaluation API — официальное описание результата оценки и его деталей.

\n

Kubernetes: Update a Deployment Without Downtime — официальная документация о проверке rollout, возврате Deployment к ревизии и ограничении rollback только Pod template.

" } diff --git a/editorial/agent-rewrites/122.json b/editorial/agent-rewrites/122.json index 56737ea..3d68833 100644 --- a/editorial/agent-rewrites/122.json +++ b/editorial/agent-rewrites/122.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-08-mechanism-feature-flags", "title": "Feature flags без рассинхрона: где принимать решение и что считать показом", "excerpt": "Как разделить серверное решение, клиентское отображение и аналитическое событие, чтобы rollout оставался объяснимым, а временная ветка действительно исчезла.", - "contentHtml": "

Новый экран включили для десяти процентов пользователей. Часть из них увидела кнопку, но API продолжил выполнять старую ветку. У другой части браузер показал новый вариант после обновления страницы, хотя сервер ещё отдавал старый. В аналитике при этом появился один общий event exposure. По нему нельзя понять, было ли решение вычислено, дошёл ли ответ до браузера и отрендерился ли экран.

\n

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

\n

Feature flag — не просто boolean. Это контракт из входов, владельца решения, версии конфигурации, безопасного значения и пути удаления. Главный принцип прост: один слой владеет eligibility, а остальные получают только тот результат, который им нужен. Evaluation, render и business effect нужно считать разными фактами.

\n

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

\n
Диагностика рассинхрона feature flag
СимптомПричинаПроверкаДействие
UI показывает новый вариант, API выполняет старыйСервер и браузер независимо вычисляют один flagСравнить evaluator, targeting key и config version в одном запросеОставить eligibility на сервере; клиенту передавать verdict и version
Один пользователь меняет вариант после логинаДо логина используется anonymous ID, после логина — account IDПостроить timeline ключа для одной сессииОписать переход ключа или закрепить вариант на время сессии
Exposure есть, но экран не показанСобытие отправляется сразу после evaluationСопоставить событие с точкой рендера и ошибками UIРазделить evaluation record и render confirmation
Отключение флага не меняет всех клиентов сразуКлиент держит кэш или обновляет конфигурацию по расписаниюПроверить effective config version и границу refreshЗадать допустимое окно устаревания и fallback
\n

Сначала разделите входы

\n

Оценка флага получает context. В него могут входить идентификатор субъекта, приложение, окружение, локаль и другие признаки. Не каждый такой признак можно передавать в браузер. Право доступа, индивидуальная цена, fraud signal и внутреннее состояние аккаунта должны оставаться за доверенной границей. Клиенту нужен ответ о доступности функции, а не правило, по которому сервер его получил.

\n

Публичный layout preference или локаль часто подходят для client-side решения. Это верно только тогда, когда они уже доступны клиенту и не скрывают защищённую политику. Если один flag зависит и от локали, и от права доступа, его нельзя безопасно перенести в браузер целиком. Сервер может вычислить eligibility, а клиент — выбрать разрешённое представление внутри полученного контракта.

\n

Один authoritative evaluator

\n

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

\n

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

\n
type FlagDecision = {\n  variant: 'control' | 'candidate';\n  enabled: boolean;\n  configVersion: string;\n};\n\nfunction resolveCheckoutBanner(input: {\n  accountId: string;\n  hasNewCheckoutAccess: boolean;\n}): FlagDecision {\n  return {\n    variant: input.hasNewCheckoutAccess ? 'candidate' : 'control',\n    enabled: input.hasNewCheckoutAccess,\n    configVersion: 'checkout-banner-v3',\n  };\n}\n\nconst decision = resolveCheckoutBanner({\n  accountId: 'account-42',\n  hasNewCheckoutAccess: false,\n});\n\n// Browser receives the result, not hasNewCheckoutAccess or the rule.\nrenderBanner({ enabled: decision.enabled, variant: decision.variant });
\n

В реальной системе значение accountId не нужно отправлять в клиент только ради отображения. Поле configVersion помогает объяснить результат и сопоставить его с журналом. Его нельзя считать глобальным доказательством: другой сервис или кэш может работать с другой версией.

\n

Отрицательный путь важнее счастливого. Если provider недоступен, context не содержит обязательного ключа или версия конфигурации устарела сверх допустимого окна, система должна вернуть явно выбранный fallback. Не следует молча вычислять тот же flag вторым алгоритмом в браузере. Иначе отказ превращается в незаметное изменение аудитории.

\n

Когорта начинается с targeting key

\n

Процент rollout стабилен только относительно ключа. Сервер может распределять пользователей по account ID, а браузер — по cookie ID. Тогда один человек получит разные варианты. После входа ключ может измениться снова. Это не мелкая деталь хеширования. Это изменение субъекта, которому принадлежит решение.

\n

Зафиксируйте четыре вещи: кто является subject, когда появляется его ключ, как проходит переход anonymous → authenticated и нужно ли закреплять вариант на активную сессию. Если конфигурация меняется, решите, может ли следующий запрос пересчитать когорту. Для некоторых экранов это допустимо. Для оплаты, миграции данных или последовательного сценария может потребоваться pinning до конца операции.

\n

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

\n

Evaluation не равно exposure

\n

Evaluation означает, что evaluator получил входы и вернул вариант или fallback. Это технический факт вычисления. Exposure candidate означает, что приложение подготовило запись о выбранном варианте. Render confirmation означает, что клиент дошёл до конкретной точки показа. Business effect означает отдельное действие пользователя или доменный результат. Эти события нельзя сливать в одно поле exposure.

\n
Что означает запись о flag
ФактМинимальные поляЧего он не доказывает
Evaluationflag key, variant, evaluator, config versionЧто UI отрендерился
Exposure candidatesubject boundary, variant, idempotency keyЧто событие доставлено ровно один раз
Render confirmationscreen, display point, client timestampЧто пользователь прочитал экран
Business effectДоменное действие и его собственный contractЧто действие вызвал только flag
\n

Слово exactly-once здесь опасно. Повторная отправка, таймаут, падение consumer и повторный запуск страницы создают разные сценарии. Если аналитике нужна дедупликация, downstream должен получить устойчивый idempotency key и окно хранения ключей. Если нужно знать факт рендера, отправляйте событие в точке рендера. Даже это не доказывает, что пользователь увидел или использовал результат.

\n

Сервер, клиент или разделённое решение

\n

Серверное решение подходит, когда flag зависит от защищённых данных или должно одинаково влиять на API и UI. Недостаток — сетевой путь и возможность увидеть старый результат из кэша. Его компенсируют явная версия, короткий контракт ответа и проверяемый fallback.

\n

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

\n

Разделённое решение полезно, когда сервер отвечает за eligibility, а клиент выбирает только presentation. Например, сервер возвращает candidate и версию, а клиент решает, какой из двух разрешённых layout показать. Граница должна быть написана рядом с контрактом. Иначе presentation постепенно начнёт повторять policy.

\n
\"Поток
Схема разделяет decision, render и event delivery. Она учебная: не показывает реальный трафик, задержки, размер когорты или результаты эксперимента.
\n

Изменение конфигурации имеет границу свежести

\n

Отключение flag не обязано мгновенно менять каждый клиент. Кэш, refresh interval, service worker, edge и повторная загрузка страницы создают окно устаревшего состояния. Поэтому контракт должен отвечать на вопрос: какая версия вернулась этому evaluator и сколько времени такой результат допустим.

\n

Если UI получает вариант с сервера, не заставляйте браузер заново применять скрытое targeting rule. Передавайте verdict, variant и config version. Если provider сообщает об изменении конфигурации, это событие помогает запустить refresh или диагностику, но само по себе не является бизнес-exposure. При ошибке обновления используйте заранее выбранное поведение: сохранить последний допустимый вариант, перейти на control или заблокировать опасную операцию. Выбор зависит от риска функции.

\n

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

\n
  1. Назовите изменение. Запишите старый путь, новый путь, безопасный fallback и доменную цель. Не смешивайте release flag, permission и эксперимент в одном ключе.
  2. Классифицируйте входы. Отделите protected facts от client-safe fields. Для каждого поля укажите, кто его видит и зачем он нужен.
  3. Выберите evaluator. Назначьте один слой владельцем eligibility. Второй слой может отображать результат, но не пересчитывает скрытое правило.
  4. Зафиксируйте targeting key. Опишите subject, переход логина, поведение после logout и политику активной сессии.
  5. Добавьте версию. Возвращайте effective config version вместе с решением. Сопоставляйте её с журналом и логами, не называя её глобальной версией системы.
  6. Разведите события. Отдельно назовите evaluation, exposure candidate, render confirmation и business effect. Для повторов задайте idempotency key.
  7. Проверьте отрицательный путь. Отключите provider, уберите targeting key и предъявите устаревшую конфигурацию. Убедитесь, что система выбирает ожидаемый fallback и не запускает скрытый второй evaluator.
  8. Откройте удаление. После выбора варианта удалите ветку, конфигурационный ключ, лишние тесты и документацию одной согласованной change. Продление срока должно иметь новую причину и новую дату review.
\n

Ограничения и критерий готовности

\n

Эта модель не выбирает конкретный SDK, TTL кэша, транспорт событий или процент rollout. Она не доказывает, что provider доступен, токен клиента ограничен или эксперимент статистически значим. Актуальная документация OpenFeature описывает provider, evaluation context и provider events как части API и жизненного цикла. Она не задаёт вашей команде SLA свежести, политику персональных данных или семантику product exposure.

\n

Учебный код выше не является production-рецептом. В боевой системе отдельно проверяют авторизацию, валидацию context, обработку ошибок, наблюдаемость и совместимость версий. Не переносите строку checkout-banner-v3 в рабочую конфигурацию без определения владельца и пути удаления.

\n

Критерий готовности проверяемый. Для одного реального flag возьмите один subject и один запрос. По логам и ответу назовите evaluator, targeting key, variant, config version и fallback. Затем покажите, где возникло evaluation, где произошёл render и как consumer обработал повтор события. Если любой ответ требует догадки или поиска правила в двух слоях, контракт ещё не готов.

\n

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

\n" + "contentHtml": "

Новый экран включили для десяти процентов пользователей. Часть из них увидела кнопку, но API продолжил выполнять старую ветку. У другой части браузер показал новый вариант после обновления страницы, хотя сервер ещё отдавал старый. В аналитике при этом появился один общий event exposure. По нему нельзя понять, было ли решение вычислено, дошёл ли ответ до браузера и отрендерился ли экран.

\n

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

\n

Feature flag — это контракт из входов, владельца решения, версии конфигурации, безопасного значения и пути удаления. В статье разберём границу между evaluation, render и business effect на учебном примере. Пример не подключается к реальному провайдеру и не выдаёт его за production-код: его задача — сделать проверяемым сам механизм.

\n

Сначала зафиксируйте симптом и границу

\n
Диагностика рассинхрона feature flag
НаблюдениеВероятная причинаЧто проверитьОграниченное действие
UI показывает candidate, API выполняет controlДва слоя независимо оценивают один ключСверить evaluator, targeting key, variant и config version в одном traceОставить eligibility на сервере и передавать verdict клиенту
Вариант меняется после входа в аккаунтДо логина используется anonymous ID, после — account IDПостроить последовательность ключей одной сессииОписать переход или закрепить вариант на операцию
Есть exposure, но экран не появилсяСобытие отправлено на evaluationСопоставить событие с точкой монтирования и ошибкой рендераРазвести evaluation и render confirmation
Отключение флага видно не всемКэш, refresh interval или edge отдают старую версиюПроверить effective config version и возраст ответаЗадать допустимое окно устаревания и fallback
\n

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

\n

Один authoritative evaluator

\n

Evaluator — компонент, который получает ключ флага и контекст и возвращает значение. Для одного бизнес-решения назначьте одного владельца eligibility (допустимости). Сервер может принять защищённые сведения о правах, тарифе или риске. Клиент после этого выбирает только разрешённое представление. Он не должен повторно решать, имеет ли пользователь право на функцию.

\n

Это не запрещает client-side flags вообще. Клиентский evaluator подходит для публичной настройки, например выбранной темы, если правило не защищает доступ и не влияет на операцию на сервере. Для смешанного правила сервер сначала возвращает безопасный verdict, а клиент может выбрать один из вариантов presentation внутри этого verdict.

\n

В ответе полезно различать минимум четыре поля:

\n
Минимальный контракт решения
ПолеСмыслЧего поле не доказывает
flagKeyКакое решение вычислялиЧто ключ не изменится завтра
variantКакой вариант вернул evaluatorЧто пользователь его увидел
configVersionС какой эффективной версией работал evaluatorЧто все сервисы уже используют эту версию
reasonПочему получено значение: targeting, split, cached, default, error и т. п.Что провайдер поддерживает каждую причину одинаково
fallbackЧто делать при ошибке или отсутствии обязательного контекстаЧто fallback безопасен для любого домена
\n

OpenFeature называет provider слоем, который разрешает значение, а подробный результат может содержать variant, reason, error code и metadata. Это удобный общий словарь, но не готовая политика вашей системы. Название configVersion в собственном API также не становится глобальной версией всех сервисов: его нужно сопоставлять с журналом публикации.

\n

Воспроизводимый evaluator и безопасный default

\n

Ниже — самостоятельный пример для Node.js 18 и новее. Он не зависит от SDK: детерминированно распределяет subject по 100 корзинам и возвращает подробный результат. Запустите его в shell как есть. Хеш нужен только для учебного стабильного распределения; он не заменяет защиту персональных данных и не доказывает статистическую значимость эксперимента.

\n
node --input-type=module <<'NODE'\nimport { createHash } from 'node:crypto';\n\nconst flagKey = 'checkout-banner';\nconst configVersion = 'checkout-banner-v3';\nconst rolloutPercent = 10;\n\nfunction bucket(targetingKey) {\n  const digest = createHash('sha256')\n    .update(flagKey + ':' + targetingKey)\n    .digest();\n  return digest.readUInt32BE(0) % 100;\n}\n\nfunction evaluate({ targetingKey, providerReady }) {\n  if (!providerReady || !targetingKey) {\n    return {\n      flagKey,\n      variant: 'control',\n      reason: providerReady ? 'missing-targeting-key' : 'error',\n      configVersion,\n      fallback: true,\n    };\n  }\n\n  const inRollout = bucket(targetingKey) < rolloutPercent;\n  return {\n    flagKey,\n    variant: inRollout ? 'candidate' : 'control',\n    reason: 'split',\n    configVersion,\n    fallback: false,\n  };\n}\n\nfor (const input of [\n  { targetingKey: 'user-001', providerReady: true },\n  { targetingKey: 'user-002', providerReady: true },\n  { targetingKey: '', providerReady: true },\n  { targetingKey: 'user-001', providerReady: false },\n]) {\n  console.log(JSON.stringify({ input, result: evaluate(input) }));\n}\nNODE
\n

Результат содержит два важных свойства. Один и тот же ключ даёт один и тот же bucket при неизменном flag key, а отсутствие ключа или отказ provider не запускает второй алгоритм: система явно возвращает control и помечает fallback. В рабочем коде fallback выбирают по риску операции. Для декоративного баннера обычно безопасен control, для платежа или миграции данных может понадобиться блокировка операции и ручная диагностика.

\n

Не передавайте в браузер hasNewCheckoutAccess, внутренний fraud signal или правило вычисления только ради рендера. Браузеру достаточно результата, который разрешён для его доверенной границы. Если provider поддерживает подробную оценку, логируйте минимально нужные поля на сервере: не превращайте targeting context в копию профиля пользователя.

\n

Targeting key определяет когорту

\n

Процент rollout стабилен только относительно ключа субъекта. Сервер, который хеширует account ID, и браузер, который хеширует cookie ID, распределяют одного человека независимо. После логина ключ может измениться ещё раз. Это не «небольшая погрешность»: изменился субъект, которому принадлежит решение.

\n

Перед запуском запишите четыре правила: кто является subject, когда появляется targeting key, как проходит anonymous → authenticated и может ли вариант измениться в активной операции. Для чтения новостей пересчёт после логина может быть приемлем. Для оформления заказа, миграции или многошаговой формы вариант часто фиксируют до завершения операции.

\n

OpenFeature описывает targeting key как строковый идентификатор субъекта и предупреждает, что провайдеры могут требовать его для дробного распределения или адресных правил. Там же есть отдельное предупреждение о персональных данных в evaluation context: провайдер может сериализовать или сохранять контекст. Поэтому используйте устойчивый технический идентификатор или согласованный хеш только после проверки модели угроз и политики хранения. Хеш сам по себе не делает значение анонимным.

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

Evaluation не равно показ

\n

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

\n

Разделите жизненный цикл на четыре факта:

\n
  1. Evaluation. Запишите flag key, variant, evaluator, config version и reason. Это ответ на вопрос «какое решение вернулось?».
  2. Render confirmation. Отправляйте его из точки, где компонент действительно смонтирован или стал видимым по выбранному критерию. Поле screen должно называть конкретную точку показа.
  3. Business effect. Отдельно фиксируйте клик, отправку формы или доменный результат. Событие не должно называться exposure, если оно описывает действие.
  4. Correlation. Свяжите записи request ID, subject boundary и безопасным variant. Для повтора доставки используйте idempotency key, но не обещайте exactly-once там, где транспорт даёт at-least-once или не даёт гарантии доставки.
\n

OpenFeature предоставляет tracking API для связывания последующего действия или состояния приложения с контекстом оценки. Это помогает анализировать влияние флага, но tracking-вызов не превращается автоматически в доказательство рендера. Семантику «виден пользователю» задаёт ваше приложение и его критерий видимости.

\n

Кэш и версия: задайте окно устаревания

\n

Отключение флага не обязано мгновенно менять каждый процесс. Старое состояние может жить в памяти SDK, edge-кэше, service worker или ответе HTTP. Поэтому для каждого флага задайте допустимое окно stale: сколько времени старый verdict приемлем и что происходит после его истечения.

\n

В HTTP кэш не должен отдавать stale-ответ без разрешения протокола или явного контракта. Но из этого не следует, что бизнес-флаг обновится мгновенно: ваши refresh interval, локальный кэш и аварийный режим всё равно требуют проектного решения. Возвращайте effective config version, а при диагностике сохраняйте возраст значения и источник кэша.

\n

При сигнале изменения конфигурации можно запустить refresh или мониторинг. OpenFeature перечисляет provider events, включая configuration changed, error и stale. Эти события говорят о состоянии provider, а не о том, что конкретный пользователь увидел новый экран. При ошибке обновления заранее выберите одно из действий: сохранить последний допустимый вариант на короткое окно, перейти в control или остановить рискованную операцию.

\n
# Пример ручной проверки контракта локального сервиса.\n# Ожидайте в JSON flagKey, variant, configVersion, reason и fallback.\nset -eu\ncurl --fail-with-body --silent --show-error \\\n  -H 'X-Debug-Subject: user-001' \\\n  'http://localhost:3000/api/flags/checkout-banner' \\\n  | jq '{flagKey, variant, configVersion, reason, fallback}'
\n

Команда не предполагает конкретный продуктовый endpoint: URL и заголовок должны существовать в вашем сервисе. Она показывает воспроизводимый минимум ручной проверки. Для двух последовательных запросов одного subject сравните variant и configVersion; затем выключите provider и проверьте, что response явно перешёл в выбранный fallback.

\n

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

\n

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

\n
  1. Зафиксируйте один тестовый targeting key и получите evaluation details. В журнале должны совпасть flag key, variant, reason и config version.
  2. Уберите targeting key. Проверьте, что система не выбирает случайный cookie и не оценивает тот же policy в браузере.
  3. Сделайте provider недоступным. Проверьте безопасный default, error code или reason и отсутствие необработанного исключения в пользовательском запросе.
  4. Измените конфигурацию. Убедитесь, что refresh меняет version в пределах согласованного окна, а старый ответ не выдаётся дольше разрешённого срока.
  5. Смонтируйте компонент с ошибкой и сравните telemetry. Evaluation может существовать, а render confirmation — отсутствовать; это разные результаты, а не потерянное поле.
  6. Повторите отправку одного business event. Проверьте дедупликацию downstream по idempotency key и отдельно измерьте потери доставки.
  7. После завершения rollout удалите ветку, ключ, лишние тесты и документацию. Оставшийся flag без срока пересмотра быстро превращается в постоянную сложность.
\n

Ограничения и критерий готовности

\n

Эта модель не выбирает SDK, транспорт аналитики, TTL, процент rollout или способ статистической проверки эксперимента. OpenFeature — vendor-neutral API: его provider может получать данные из разных источников, а конкретная гарантия свежести, хранения контекста и доставки tracking зависит от реализации. RFC 9111 описывает HTTP-кэширование, но не задаёт вашей бизнес-семантике безопасный fallback.

\n

Учебный SHA-256 evaluator не является доказательством равномерного эксперимента: он показывает детерминированную корзину, но не проверяет качество выборки, SRM, мощность теста или причинность метрики. Не используйте этот фрагмент как замену провайдеру с управлением конфигурацией. Не считайте render confirmation доказательством внимания пользователя и не называйте доставку exactly-once без подтверждённой семантики транспорта.

\n

Flag готов к rollout, если для одного реального subject команда может без догадок назвать evaluator, targeting key, variant, config version, reason и fallback; показать точку render; воспроизвести отказ provider; увидеть границу stale; связать бизнес-эффект с evaluation. Если правило приходится искать одновременно в сервере, браузере и конфигурационном файле, сначала сократите число владельцев.

\n

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

\n" } diff --git a/editorial/agent-rewrites/123.json b/editorial/agent-rewrites/123.json index 2e7f112..0a278b0 100644 --- a/editorial/agent-rewrites/123.json +++ b/editorial/agent-rewrites/123.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-08-practice-feature-flags", "title": "Фича-флаг как контракт выпуска: включить, проверить, удалить", "excerpt": "Release-флаг снижает риск только тогда, когда у него есть ограниченная аудитория, безопасный fallback, владелец, срок пересмотра и заранее описанное удаление.", - "contentHtml": "

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

\n

Цена такой ошибки растёт не вместе с процентом rollout. Она растёт с каждым новым условием, исключением и сервисом, который читает тот же ключ. Выключенный флаг уменьшает аудиторию, но не убирает ветки, не возвращает изменённые данные и не объясняет, почему система выбрала вариант. Тезис статьи простой: release-флаг — это временный контракт выпуска. Он должен описывать границу решения, безопасное значение, наблюдение и момент, когда код исчезнет.

\n

Механизм: boolean скрывает решение

\n

Значение true или false отвечает только на один вопрос: какой путь выбрать сейчас. Выпуск требует ответов на другие вопросы. Для кого действует правило? Где его вычисляют? Что произойдёт при ошибке провайдера или устаревшей конфигурации? Кто остановит rollout? Как проверить новый путь? Что удалить после выбора варианта?

\n

Удобно разделить флаг на пять частей. Owner принимает решение на review. Audience задаёт стабильную когорту и окружение. Fallback определяет путь при недоступном или сомнительном решении. Signals показывают, что именно произошло. Cleanup связывает итоговый вариант с удалением условий, настроек и тестовых исключений.

\n

Эти части не заменяют flag management system. Они задают контракт вокруг неё. Провайдер может вернуть default value, сообщить об ошибке или показать, что его состояние устарело. Приложение всё равно должно решить, какое значение безопасно для конкретной операции. Для цены, права доступа и другого защищённого решения fallback выбирает сервер. Клиент получает уже вычисленный результат, а не правило с закрытым контекстом.

\n

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

\n
СимптомПричинаПроверкаДействие
«Временный» флаг появляется без даты удаленияBoolean приняли за весь контракт выпускаНайдите owner, review date и список ветокОформите карточку и назначьте review до rollout
После выключения ошибки исчезли, но код не меняетсяDisablement перепутали с cleanupСравните ветки, config keys, тесты и формат записанных данныхОткройте отдельное удаление и оставьте fallback до его завершения
Один пользователь видит разные вариантыСервисы используют разные targeting keys или версии конфигурацииЗапишите key, effective version и границу сессии в evaluation detailsНазначьте один authoritative evaluator или явно опишите split decision
Событие exposure есть, а интерфейс не показалсяEvaluation назвали эффектомРазделите evaluation, response, render и user actionПереименуйте сигнал и не делайте вывод о результате по одной записи
Провайдер недоступенFallback не определён для ошибки или stale stateВоспроизведите provider error в изолированной проверкеВерните объявленный safe value и поднимите сигнал оператору
\n

Карточка до первой строки кода

\n

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

\n
const releaseFlagCard = {\n  key: 'synthetic-checkout-copy-v1',\n  class: 'release',\n  owner: 'synthetic-checkout-owner',\n  audience: 'synthetic-internal-beta-cohort',\n  targetingKey: 'synthetic-subject-id',\n  fallback: 'existing-checkout-copy',\n  reviewAt: 'synthetic-2026-09-30',\n  signals: ['evaluation-outcome', 'client-render-error'],\n  cleanup: ['freeze-variant', 'remove-branches', 'remove-config'],\n};\n\n// Учебная запись. Она не создаёт флаг, не читает provider\n// и не отправляет telemetry или данные пользователя.
\n

В примере все значения synthetic. Дата не запускает таймер, ключ не определяет настоящую когорту, а массив cleanup не удаляет файлы. Он показывает форму review. В реальной системе карточка должна ссылаться на конкретные владельца, окружение, конфигурацию и проверку, но не должна содержать секреты.

\n

Где принимать решение

\n

Сервер должен владеть решением, если оно зависит от entitlement, цены, account state, fraud signal или другого защищённого входа. Сервер возвращает клиенту presentation value и, при необходимости, effective configuration version. Браузер не должен повторять правило по урезанному контексту: такое дублирование создаёт два источника истины.

\n

Клиент может вычислять presentation-флаг, если все входы уже публичны: например, доступная локаль, опубликованный вариант оформления или локальная настройка layout. Это сокращает сетевой путь, но не отменяет проверку token, CORS, cache и refresh semantics конкретного провайдера.

\n

Разделяйте evaluation и exposure. Evaluation означает, что evaluator вернул вариант для заданного контекста. Exposure можно использовать для записи попытки показать вариант, но запись не доказывает успешный render, пользовательское действие или бизнес-эффект. Если событие доставляется повторно или теряется, это нужно учитывать в consumer и в выводах из метрик.

\n
\"Дерево
Сначала проверяется контракт, затем выбирается граница решения. Схема учебная: она не показывает состояние реального flag-сервиса, rollout или production-метрики.
\n

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

\n
  1. Назовите старый и новый путь. Запишите, какая функция меняется, что остаётся fallback и какие данные каждый путь читает или записывает.
  2. Выберите класс флага. Не смешивайте release, эксперимент, миграцию данных и постоянную policy в одном ключе. Для каждого класса нужны разные сроки и проверки.
  3. Назначьте owner и review date. Owner должен выбрать итоговый вариант, продлить контракт с причиной или начать cleanup. Продление без новой причины не является нейтральным действием.
  4. Опишите audience boundary. Зафиксируйте environment, targeting key, способ удержания когорты и данные, которые не покидают сервер. Проверьте поведение после логина, смены сессии и обновления конфигурации.
  5. Определите fallback и отрицательный путь. Проверьте default value, provider error, stale state, таймаут и недоступность сети. Убедитесь, что возврат не скрывает несовместимые данные.
  6. Назовите сигналы и их пределы. Разделите evaluation result, render error, API failure и user action. Для каждого напишите, какой вывод он разрешает и какой запрещает.
  7. Расширяйте аудиторию постепенно. На каждом шаге проверяйте ошибку, latency, целостность ответа и работу старой ветки. Если сигнал нарушает stop condition, верните объявленный fallback и зафиксируйте причину.
  8. Зафиксируйте выбранный вариант. После решения не добавляйте новые rules в старый release-флаг. Создайте cleanup change для кода, конфигурации, тестов, документации и лишних сигналов.
\n

Как проверить удаление

\n

Выключение и удаление проходят разными проверками. После выключения убедитесь, что новый путь не получает аудиторию и старый путь отвечает за заявленный контракт. Затем проверьте, не остались ли записи нового формата, client cache, server cache, отдельные разрешения и тестовые обходы. Только после этого удаляйте ветки. Иначе «cleanup» может убрать защитное условие раньше, чем система перестанет читать его последствия.

\n

Простой критерий для pull request: поиск по ключу флага не находит production-условий после удаления, тесты больше не выбирают вариант через старую конфигурацию, а единственный оставшийся путь не зависит от временного default. Если данные менялись, добавьте отдельную проверку совместимости. Не объявляйте cleanup завершённым по одному зелёному unit-тесту.

\n

Ограничения

\n

Карточка не выбирает безопасный процент rollout и не заменяет threat model, approval, миграцию схемы, SLA или план отката данных. OpenFeature задаёт API evaluation context, targeting key, providers и provider events, но не навязывает поля owner, reviewAt или cleanup. Эти поля — локальная инженерная policy.

\n

Разные провайдеры по-разному обновляют конфигурацию, кэшируют правила и обрабатывают ошибки. Нельзя переносить интервал refresh или семантику токена одного SDK в другой без проверки его документации. Нельзя считать targeting key доказательством единой когорты во всех сервисах, если они не используют общую версию конфигурации и один authoritative evaluator.

\n

Учебный объект выше не является результатом production-эксперимента. Он не доказывает latency, delivery rate, безопасность данных или полезный эффект новой формы. Production-вывод требует реальных наблюдений, определённого окна измерения и заранее согласованного stop condition.

\n

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

\n

Проверяемая готовность

\n

Флаг готов к включению, если другой инженер без устного контекста может назвать его вопрос, owner, audience boundary, authoritative evaluator, safe fallback, signals, stop condition и review date. Флаг готов к удалению, если выбран вариант зафиксирован, поиск не находит временных веток, тесты не зависят от старого ключа, а изменения данных проверены отдельно. Если хотя бы один ответ отсутствует, это не повод добавить ещё один boolean. Это сигнал вернуть контракт на review.

" + "contentHtml": "

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

\n

Выключенный флаг уменьшает аудиторию, но не удаляет код и не откатывает побочные эффекты. Поэтому release-флаг нужно рассматривать как временный контракт выпуска: он отвечает на вопрос, кто получает новый путь, какое значение безопасно при сбое, какие сигналы останавливают rollout и когда исчезнут условие, настройка и лишние тесты.

\n

Какая задача решается

\n

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

\n

До первой строки кода зафиксируйте один вопрос. Формулировка «включить новый checkout» слишком широкая. Проверяемый вариант звучит так: «Показать новую форму внутренней когорте, а при ошибке оценки или рендера оставить прежнюю форму». В такой фразе уже видны субъект, граница решения и отрицательный путь.

\n
Часть контрактаЧто зафиксироватьКак проверить
ВопросКакое поведение меняется и для когоДругой инженер пересказывает условие одним предложением
ВладелецКто остановит rollout и примет итоговый вариантИмя или команда указаны в карточке изменения
АудиторияОкружение, targeting key и способ удержания когортыОдин субъект получает ожидаемый вариант повторно
FallbackПуть при ошибке, таймауте и устаревшем состоянииСбой провайдера воспроизведён в изолированной проверке
УдалениеКод, настройка, тесты и сигналы, которые исчезнутПосле cleanup поиск по ключу не находит рабочих веток
\n

Что именно возвращает провайдер

\n

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

\n

Контекст оценки содержит сведения, по которым выбирается вариант. В нём есть необязательный targetingKey — строковый идентификатор субъекта. Он может быть идентификатором пользователя, сервиса или тестового субъекта. Если провайдер использует процентное распределение или адресные правила, отсутствие ключа может сделать результат непредсказуемым. Это свойство провайдера, а не гарантия всех систем фича-флагов.

\n

Условие применимости здесь важнее названия библиотеки. OpenFeature описывает API и жизненный цикл интеграции, но не назначает владельца, срок удаления, процент rollout или безопасный fallback для бизнеса. Эти поля нужно определить в своей change-карточке и проверить средствами конкретного SDK.

\n
const flagCard = {
  key: 'checkout-form-v2',
  kind: 'release',
  owner: 'checkout-team',
  environment: 'staging',
  targetingKey: 'test-subject-001',
  defaultValue: false,
  fallbackPath: 'checkout-form-v1',
  stopConditions: ['5xx increase', 'render error', 'latency budget'],
  reviewAfter: '2024-08-30',
  cleanup: ['remove-branch', 'remove-config', 'keep-regression-test'],
};

if (flagCard.defaultValue !== false || !flagCard.cleanup.length) process.exit(1);
console.log(flagCard.key);
\n

Сохраните фрагмент в файл flag-card.mjs, затем выполните node flag-card.mjs. Команда проверит, что fallback по умолчанию выключен и список удаления не пуст. Файл не обращается к настоящему провайдеру и не отправляет телеметрию. Поля owner, reviewAfter и cleanup — локальная политика команды, а не часть обязательного API OpenFeature.

\n

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

\n

Где вычислять решение

\n

Решение, зависящее от тарифа, права доступа, состояния счёта, fraud-сигнала или другого защищённого входа, вычисляйте на сервере. Клиент должен получить уже разрешённый presentation value и обработать его как вход, а не повторять правило по урезанному контексту. Иначе сервер и браузер могут выбрать разные варианты.

\n

Клиентская оценка подходит для публичного оформления, локали или другого решения, для которого все входы уже открыты и потеря точности не меняет права пользователя. Даже в этом случае проверьте семантику кэша, обновления и отсутствие чувствительных данных в evaluation context. Название «client-side» не означает, что конфигурация автоматически безопасна.

\n
Дерево решения release-флага: от симптома и проверки карточки через границу серверного или клиентского решения к наблюдению и удалению временной ветки
Сначала фиксируются владелец, аудитория и fallback, затем выбирается граница оценки. Диаграмма показывает порядок review, а не состояние конкретного сервиса и не обещает результат rollout.
\n

Evaluation, exposure и результат — разные события

\n

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

\n

Разделите минимум четыре сигнала:

\n
  1. evaluation. Запишите ключ флага, вариант или причину, версию конфигурации, окружение и обезличенный идентификатор когорты;
  2. response. Проверьте, что сервер вернул допустимый ответ и не скрыл ошибку под успешным HTTP-статусом;
  3. render. Зафиксируйте, что выбранный компонент действительно построился без ошибки;
  4. action. Отдельно измеряйте действие пользователя или бизнес-результат, если он нужен для решения.
\n

События провайдера полезны для состояния самого источника: OpenFeature описывает готовность, ошибку, изменение конфигурации и устаревшее состояние. Событие PROVIDER_STALE говорит о свежести кэша, но не доказывает exposure. Аналогично, событие evaluation не доказывает успешный render. В каждом дашборде укажите, какой вывод разрешает сигнал и какой вывод из него делать нельзя.

\n

Порядок безопасного rollout

\n

Rollout — это последовательность проверяемых шагов, а не одно изменение процента. Сначала убедитесь, что старый путь по-прежнему поддерживается и что оба пути согласованы с форматом данных. Затем расширяйте аудиторию только при выполнении заранее заданных stop conditions.

\n
  1. Назовите старый и новый путь. Перечислите чтения, записи, побочные эффекты и совместимость форматов.
  2. Выберите тип флага. Не смешивайте release, эксперимент, миграцию данных и постоянную policy в одном ключе: у них разные сроки и критерии успеха.
  3. Закрепите субъекта. Используйте стабильный targeting key и проверьте повторную оценку после новой сессии, логина и смены окружения.
  4. Определите fallback. Отдельно проверьте default value, ошибку провайдера, таймаут, stale state и частично обновлённую конфигурацию.
  5. Проведите малый шаг. Сравните error rate, latency, целостность ответа и ошибки старой ветки с базовым окном. Не приписывайте разницу флагу без одинаковой границы измерения.
  6. Остановите rollout при нарушении условия. Верните объявленный fallback, запишите причину и сохраните effective version, чтобы повторить диагностику.
  7. Зафиксируйте итог. После выбора варианта заморозьте правило и откройте отдельное изменение cleanup.
\n

Вариант «внутренняя когорта» не означает автоматически безопасный вариант. Проверьте, что в неё не попадают реальные клиенты, что персональные данные не используются как необязательный targeting input и что оператор может быстро вернуть старое поведение.

\n

Отключение не равно удалению

\n

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

\n
rg -n --hidden --glob '!node_modules' 'checkout-form-v2' src test config
rg -n --hidden --glob '!node_modules' 'checkout-form-v1|checkout-form-v2' src test config
git diff --check
pnpm test --filter checkout
\n

Команды предполагают Unix-подобную оболочку, установленный rg, рабочий скрипт тестов и каталог src. Подставьте имена своего репозитория. Первый поиск показывает остатки ключа, второй помогает найти обе ветки до удаления. Если новая ветка записывала данные, добавьте проверку чтения старого и нового формата; одного поиска и зелёного unit-теста недостаточно.

\n

Надёжный cleanup оставляет тест поведения выбранного варианта, но удаляет временную развилку и её настройку. Отдельно обновите runbook и мониторинг. Если поиск всё ещё находит production-условие по ключу, удаление не закончено.

\n

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

\n

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

\n

Интервалы обновления, поведение при сетевой ошибке, кэш и формат resolution details зависят от провайдера и SDK. Нельзя переносить настройки одного продукта в другой по названию поля. Для OpenFeature гарантии относятся к контракту API; owner, review date, stop condition, redaction и cleanup команда должна определить и протестировать самостоятельно.

\n

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

\n

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

\n\n

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

\n

Флаг готов к включению, если инженер может без устного контекста назвать вопрос, старый и новый путь, owner, аудиторию, targeting key, серверную или клиентскую границу, fallback, stop conditions, сигналы и дату review. Флаг готов к удалению, если выбранный вариант зафиксирован, совместимость данных проверена, поиск не находит временных веток, а тесты больше не зависят от старого ключа.

\n

Если не известен хотя бы один из этих ответов, добавление ещё одного boolean не уменьшит риск. Сначала верните изменение на review и заполните контракт.

" } diff --git a/editorial/agent-rewrites/124.json b/editorial/agent-rewrites/124.json index 7a7de62..f2c7425 100644 --- a/editorial/agent-rewrites/124.json +++ b/editorial/agent-rewrites/124.json @@ -2,6 +2,6 @@ "index": 124, "slug": "editorial-2024-07-field-release-engineering", "title": "Когда версия не доказывает релиз: проверка artifact, migration и rollback", - "excerpt": "Один номер релиза может скрывать разные commit, artifact и migration. Разбираем, где остановиться, какие связи проверить и почему возврат образа не отменяет изменения данных.", - "contentHtml": "

В заявке на выпуск стоит 2024.07.0. Такой же номер виден у commit, container image, migration и rollout. После выкладки сервис отвечает кодом старой схемы: новый код ждёт поле, которого в базе нет. Команда повторяет deploy, потому что все карточки выглядят согласованными. Ошибка становится дороже с каждой попыткой: растёт окно недоступности, меняется состояние данных, а точку возврата уже трудно назвать.

\n

Проблема не в самом номере версии. Проблема в том, что номер заменил связи между объектами. Он не доказывает, что artifact собран из нужного commit, что migration рассчитана на этот contract и что rollout ссылается на тот же digest. Выпуск готов только тогда, когда эти связи можно проверить по точным значениям, а отрицательный результат останавливает действие.

\n

Где рвётся цепочка

\n

Commit описывает исходный revision. Artifact содержит собранное содержимое и immutable digest. Migration меняет схему или данные и должна назвать целевую версию и совместимость. Rollout intent говорит, какой digest команда собирается отправить. Return point хранит предыдущую версию и digest. Эти записи связаны, но не заменяют друг друга.

\n

У каждой связи есть проверяемое утверждение. Artifact должен ссылаться на exact commit id. Migration должна называть release version и совместимость с текущей схемой. Rollout должен содержать digest из artifact, а не только tag. Return point должен быть известен до approval. Если одно утверждение ложно или неизвестно, действие заканчивается на gate. Retry не исправляет неправильную запись.

\n
\"Схема
Учебная схема показывает порядок сверки. Она не является логом CI, registry, кластера или production rollout.
\n

Минимальный пример

\n

Ниже — ограниченный учебный пример. Значения вымышлены. Код не обращается к Git, registry, CI, Kubernetes API или базе данных. Он только сравнивает заранее заданные записи и возвращает решение для проверки человеком.

\n
const release={version:'2024.07.0'},commit={id:'commit-7f4a0c1'},artifact={digest:'sha256:release-070-a1',sourceCommitId:'commit-7f4a0c1'},migration={targetReleaseVersion:'2024.07.0',compatibleWith:'2024.06.3'},rollout={requestedArtifactDigest:'sha256:release-070-a1',migrationVersion:'2024.07.0'}; const checks={source:artifact.sourceCommitId===commit.id,migration:migration.targetReleaseVersion===release.version,artifact:rollout.requestedArtifactDigest===artifact.digest,rollout:rollout.migrationVersion===migration.targetReleaseVersion}; const ready=Object.values(checks).every(Boolean); if(!ready) throw new Error('stop: reconcile release records');
\n

При ready === true пример говорит только о согласованности пяти записей. Он не говорит, что образ существует, подпись действительна, migration выполнена или сервис здоров. Если заменить sourceCommitId на другой id, результат должен стать отрицательным. То же относится к digest и target version. Это и есть полезный отрицательный путь: система не угадывает, какую запись считать правильной.

\n

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

\n
Диагностика расхождений до запуска
СимптомПричинаПроверкаДействие
Номер версии совпадает, но artifact указывает на другой commit.Tag используют вместо точной связи с исходным revision.Сравнить artifact.sourceCommitId и commit id.Остановить выпуск. Исправить запись или пересобрать artifact после решения владельца.
Код можно вернуть, но схема базы уже изменилась.Rollback binary ошибочно считают rollback данных.Проверить migration target, compatibility и обратную процедуру.Вернуть только явно разрешённый artifact; вопрос данных передать отдельному владельцу.
Rollout прошёл с тем же tag, но другим digest.Intent ссылается на mutable label, а не на immutable content.Сравнить requested digest с digest artifact.Не запускать rollout. Пересоздать intent после сверки.
После stop команда предлагает повторить deploy.Retry используют как замену объяснению расхождения.Найти первую ложную связь и назвать её источник.Сначала reconcile records, затем повторить только проверку.
\n

Таблица разделяет четыре разных вопроса. Mismatch commit относится к происхождению artifact. Mismatch migration относится к совместимости contract. Mismatch digest относится к содержимому, выбранному для rollout. Повторная попытка без такой классификации стирает причину и оставляет команду без доказуемого решения.

\n

Порядок действий перед выпуском

\n
  1. Зафиксируйте границу проверки. Укажите release version, owner и источник каждой записи. Пометьте, что сейчас выполняется review, а не deploy.
  2. Сверьте commit и artifact. Проверьте exact source commit и digest. Название ветки, последний merge и короткий tag не заменяют id.
  3. Опишите migration отдельно. Назовите target version, совместимость с текущим contract и действие по данным. Не прячьте migration в комментарии к image.
  4. Сверьте rollout intent. Он должен повторять immutable digest artifact и migration version. Любое расхождение ведёт в stop.
  5. Назовите return point. Запишите предыдущую версию и digest. Отдельно укажите, что произойдёт с данными и кто проверит этот путь.
  6. Повторите проверки после исправления. Передайте человеку только записи без ложных связей. Положительный результат открывает review, но не выдаёт автоматическое разрешение на deploy.
\n

Почему rollback не возвращает всё

\n

Rollback Deployment обычно возвращает предыдущую ревизию Pod template. Это полезно для кода и настроек, которые входят в template. Оно не отменяет произвольный SQL, удалённую запись, заполненное поле или изменение внешнего contract. Если migration уже прошла, старый image может не уметь читать новую схему.

\n

Return point должен содержать две границы. Первая — какую версию artifact можно запустить. Вторая — что разрешено делать с данными. Возможны обратная migration, совместимый промежуточный код, восстановление из резервной копии или запрет автоматического возврата. Пока путь не проверен, честная формулировка звучит так: «возврат artifact определён; откат данных не доказан».

\n

То же различие действует для provenance и attestations. Официальная документация SLSA описывает проверку provenance через сравнение с ожиданиями пакета. GitHub описывает artifact attestations как подписанные claims о происхождении и даёт команды для проверки. Ни один из этих механизмов сам по себе не утверждает, что migration совместима, rollout одобрен или production здоров.

\n

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

\n

Описанный подход ловит расхождения между названными записями. Он не доказывает, что значения правдивы. Он не проверяет историю Git, содержимое image, подпись, policy CI, права на кластер, runtime configuration, состояние базы, трафик, метрики или факт доставки. Для этих вопросов нужны разрешённые источники и отдельные проверки.

\n

Если commit неизвестен, digest отсутствует, migration не имеет compatibility statement или return point не назван, результат должен быть отрицательным. Не подставляйте «последний main», не ищите image по tag и не объявляйте data rollback по факту отката Pod template. Остановитесь на первой неизвестной границе. Такое поведение медленнее одной зелёной кнопки, но дешевле расследования после повреждения данных.

\n

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

\n

Материал готов к передаче на human review, если второй инженер без устных пояснений может показать: exact commit id, artifact digest, связь artifact с commit, migration target и compatibility, rollout digest, migration version и return point. Для каждой строки есть источник, владелец и действие при mismatch. Проверка должна дать либо все утверждения true, либо конкретный stop с названием ложной связи. В первом случае разрешение на deploy всё ещё принимает авторизованный процесс. Во втором случае deploy не начинается.

\n

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

\n

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

\n" + "excerpt": "Как связать commit, provenance, digest, migration и точку возврата до approval — с воспроизводимым gate, отрицательным тестом и честными границами rollback.", + "contentHtml": "

В заявке на выпуск стоит 2024.07.0. Такое же имя указано у ветки, образа, миграции и выкладки. После запуска сервис отвечает ошибкой схемы: новый код обращается к полю, которого нет в базе. Команда повторяет deploy, потому что все записи выглядят согласованными. Это учебный сценарий, а не отчёт о конкретной production-системе. Его задача — показать, какие факты нужно связать до разрешения релиза.

\n

Номер версии не доказывает происхождение и содержимое выпуска. Для решения нужны как минимум четыре независимые связи: artifact собран из нужного commit, rollout выбирает тот же digest, migration совместима с текущей схемой, а return point описан отдельно. Если связь неизвестна или ложна, проверка должна остановиться. Повторный deploy не превращает неизвестный факт в доказательство.

\n

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

\n

Commit — точная ревизия исходников. Artifact — результат сборки, например контейнерный образ, доступный по digest. Provenance — проверяемые сведения о том, где и как собран artifact. Migration — изменение схемы или данных с заявленной совместимостью. Rollout intent — запись о том, какой artifact и какую migration собираются применить. Return point — заранее известный вариант возврата к предыдущему коду и описание границы данных.

\n

Это не одна сущность с полем version. Для каждого объекта нужен источник и владелец. Поле artifact.sourceCommitId ниже — проектный контракт учебной модели, а не универсальное поле Docker или Kubernetes. Аналогично, migration.compatibleWith требует договорённости команды: инструменты миграций называют такие сведения по-разному.

\n
\"Схема
Учебная схема показывает порядок сверки и стоп-ветку. Это не лог CI, registry, кластера, базы данных или реально выполненного rollout.
\n

Почему одного tag недостаточно

\n

Tag удобен для человека, но это изменяемая ссылка. Один и тот же tag может указывать на другой образ после следующей сборки. Digest адресует содержимое образа. Docker документирует загрузку образа по форме name@sha256:... и объясняет, что такой идентификатор фиксирует выбранную версию содержимого. Поэтому в rollout-записи храните digest, а tag оставляйте только как дополнительную подпись для чтения.

\n

Digest отвечает лишь на вопрос «какое содержимое выбрано». Он не отвечает на вопросы «из какого commit оно собрано», «кто его собрал» и «совместима ли схема». Для этого нужен provenance и политика проверки. В SLSA v1.2 проверка включает сопоставление subject с digest artifact, доверенный builder, подпись и ожидаемые параметры сборки. Это отдельный gate, а не синоним успешной загрузки образа.

\n

У attestation тоже есть граница. Подписанное утверждение связывает metadata с artifact, но не утверждает, что migration выполнилась, сервис принимает трафик или решение о выкладке одобрил нужный человек. Эти факты должны появиться в собственных системах и проверках.

\n

Минимальный воспроизводимый gate

\n

Следующая команда запускается в Node.js без зависимостей. Все значения синтетические: она не обращается к Git, registry, CI, Kubernetes API или базе. Код проверяет только заранее подготовленные записи и завершает процесс с ненулевым статусом при расхождении. Сохраните его как команду через heredoc или вставьте в локальный терминал.

\n
node --input-type=module <<'NODE'\nconst current = { schemaVersion: '2024.06.3' };\nconst release = { version: '2024.07.0' };\nconst commit = { id: 'commit-7f4a0c1' };\nconst artifact = {\n  digest: 'sha256:release-070-a1',\n  sourceCommitId: 'commit-7f4a0c1',\n};\nconst migration = {\n  version: '2024.07.0',\n  targetSchemaVersion: '2024.07.0',\n  compatibleWith: '2024.06.3',\n};\nconst rollout = {\n  artifactDigest: 'sha256:release-070-a1',\n  migrationVersion: '2024.07.0',\n};\nconst returnPoint = {\n  artifactDigest: 'sha256:release-069-z9',\n  schemaVersion: '2024.06.3',\n};\n\nconst checks = {\n  source: artifact.sourceCommitId === commit.id,\n  release: migration.version === release.version,\n  compatibility: migration.compatibleWith === current.schemaVersion,\n  artifact: rollout.artifactDigest === artifact.digest,\n  migration: rollout.migrationVersion === migration.version,\n  returnPoint: Boolean(returnPoint.artifactDigest && returnPoint.schemaVersion),\n};\nconst ready = Object.values(checks).every(Boolean);\n\nconsole.log(JSON.stringify({ checks, ready }, null, 2));\nif (!ready) process.exitCode = 1;\nNODE
\n

При исходных данных команда печатает \"ready\": true и завершается с кодом 0. Измените rollout.artifactDigest на sha256:release-070-other: поле artifact станет false, а процесс завершится с кодом 1. Такой отрицательный тест важнее красивого положительного fixture: он показывает, что gate не угадывает правильную запись.

\n

Положительный результат означает только согласованность шести полей в памяти. Он не доказывает, что digest существует в registry, provenance подписан, миграция запущена, а старый artifact умеет работать с новой схемой. Эти вопросы нельзя «досчитать» из примера.

\n

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

\n
Классификация расхождения до запуска
СимптомПричинаПроверкаДействие
Tag совпадает, а digest другой.Rollout использует изменяемую ссылку вместо содержимого.Сверить digest в intent с digest из registry и записи сборки.Остановить запуск; пересоздать intent после выбора exact artifact.
Artifact указывает на другой commit.Образ собран из другой ревизии, либо provenance неполон.Сопоставить commit, provenance subject и ожидаемый source repository.Не объявлять образ релизом; пересобрать или исправить запись после расследования.
Новая схема не совместима со старым кодом.Миграцию выполнили как необратимый шаг до перехода reader/writer.Проверить порядок expand, переключение читателей и contract-шаг.Остановить удаление/изменение данных; привлечь владельца схемы.
После stop предлагают повторить deploy.Retry используют вместо объяснения первой ложной связи.Найти первый mismatch и назвать источник каждого значения.Сначала reconcile записей, затем повторить только проверку.
\n

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

\n

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

\n
  1. Определите объект релиза. Зафиксируйте имя сервиса, среду, release version и владельцев commit, artifact, migration и deploy. Не смешивайте тестовые и production-записи.
  2. Сверьте исходники и сборку. Возьмите полный commit id из системы контроля версий. Проверьте source repository, builder и параметры provenance; короткий tag или название ветки их не заменяет.
  3. Зафиксируйте artifact. Получите digest из registry или результата push. В deployment-манифесте используйте ссылку с digest, если это поддерживает ваша платформа. Для Docker-пути пример выглядит как registry.example/app@sha256:....
  4. Опишите migration. Назовите текущую и целевую схему, совместимость reader/writer, порядок шагов и отдельное действие для данных. Не прячьте SQL и условия возврата в комментарии к образу.
  5. Сверьте intent. Digest artifact и digest rollout должны совпасть, как и версия migration. Несовпадение переводит запись в stop.
  6. Проверьте return point. Запишите предыдущий digest, revision, способ переключения и проверку здоровья. Отдельно укажите, можно ли вернуть данные, и кто принимает это решение.
  7. Повторите gate после исправления. Положительный результат открывает human approval, но не выдаёт автоматическое право на deploy. Авторизованный процесс всё равно должен проверить свои policy, права и наблюдаемость.
\n

Что действительно означает rollback

\n

В Kubernetes новая revision Deployment появляется, когда меняется Pod template, например image или labels. Команда kubectl rollout undo возвращает предыдущую ревизию этого шаблона. Значит, такой rollback касается описания Pod: образа, переменных и других полей template. Он не является отменой произвольного SQL, удалённой строки, уже отправленного сообщения или изменения внешней системы.

\n

Проблема особенно заметна при миграции. Если новая версия добавила поле и старый код его не ожидает, возврат image может вернуть работоспособность. Если новая версия удалила или изменила смысл поля, старый код может не запуститься на текущей схеме. До релиза нужен либо совместимый expand/contract-переход, либо проверенная обратная миграция, либо восстановление из резервной копии. Выбор зависит от базы, инструмента и договора владельцев данных.

\n

Практичная формулировка return point состоит из двух утверждений: «этот artifact можно снова запустить» и «для этой схемы есть разрешённое действие». Первое не даёт права утверждать второе. Если доказан только возврат Pod template, так и пишите в release record: «rollback кода определён; rollback данных не подтверждён».

\n

Границы применимости и безопасный stop

\n

Метод подходит как предварительная проверка связей в release record и как шаблон для CI-gate. Он не заменяет security review, проверку подписи, тест совместимости, backup/restore drill, smoke-тест, анализ метрик или approval согласно правилам организации. Названия полей и формат provenance в вашем toolchain могут отличаться; переносите инварианты, а не имена из учебного примера.

\n

SLSA проверяет provenance относительно заданных ожиданий, но сами ожидания должны быть сформированы и защищены командой. GitHub artifact attestations доступны только при соответствующей настройке workflow и permissions; команда gh attestation verify требует доступного GitHub-контекста и не проверяет вашу migration. Kubernetes хранит историю Deployment с ограничениями revision history, поэтому старый Pod template может быть недоступен, если историю сократили или образ удалён.

\n

Статус должен быть отрицательным, если отсутствует полный commit id, digest, источник provenance, compatibility statement или return point. Не подставляйте «последний main», не ищите образ по tag и не объявляйте rollback данных по факту kubectl rollout undo. Остановка на первой неизвестной границе дешевле расследования после повреждения данных.

\n

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

\n

Перед human approval второй инженер без устных пояснений должен найти exact commit id, source repository, artifact digest, связь artifact с commit, результат provenance verification, текущую и целевую схему, compatibility statement, rollout digest, migration version и return point. Для каждого значения указаны источник, владелец и действие при mismatch.

\n

Готовность здесь — не одно зелёное число. Это воспроизводимый набор утверждений: все обязательные связи истинны, неизвестные значения не замаскированы, а возврат коду не выдан за возврат данным. Если хотя бы одно утверждение нельзя показать, релиз остаётся на проверке с конкретной причиной остановки.

\n

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

\n" }