На 31 июля 2026 года строгий аудит проходит 238 из 358 созданных материалов. Остальные 120 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 241 из 358 созданных материалов. Остальные 117 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
P78 заменяет только три августовских черновика через overlay `web/scripts/upgrade-2024-08.mjs`:
-`editorial-2024-08-practice-feature-flags` — «Фича-флаг как контракт выпуска: как не оставить переключатель навсегда»;
-`editorial-2024-08-mechanism-feature-flags` — «Где принимать решение флага: server, client и журнал exposure»;
-`editorial-2024-08-field-feature-flags` — «Выключить флаг — не значит удалить риск: rollout, rollback и cleanup».
Скрипт экспортирует только `revisions`; `web/data/articles.json` не менялся. В пакете нет registry, README, статуса, staged changes, commit или push. Фикстура работает только с versioned fixed synthetic objects в памяти: она не читает и не пишет flag store, файлы, сеть, часы, CI, telemetry, user data или production.
## Source report и историческая граница
| Источник | Закрепление и факт, используемый в тексте | Ограничение |
| --- | --- | --- |
| [OpenFeature Specification v0.6.0 release, 16.05.2023](https://github.com/open-feature/spec/releases/tag/v0.6.0) | Датированный versioned release, доступный к августу 2024; включает provider events, initialization и shutdown. | Не доказывает одинаковую реализацию provider и не задаёт exactly-once для какого-либо события. |
| [OpenFeature v0.6.0 — Flag Evaluation API](https://github.com/open-feature/spec/blob/v0.6.0/specification/sections/01-flag-evaluation.md) | Раздел versioned release: typed evaluation с flag key, default value и optional evaluation context; abnormal execution возвращает default. | API-контракт, не policy TTL, секретов, rollout или удаления флага. |
| [OpenFeature v0.6.0 — Evaluation Context](https://github.com/open-feature/spec/blob/v0.6.0/specification/sections/03-evaluation-context.md) | Раздел versioned release: targeting key идентифицирует subject, а context имеет определённый merge order. | Раздел experimental; не обещает distributed snapshot или непрерывную консистентность когорты. |
| [OpenFeature v0.6.0 — Events](https://github.com/open-feature/spec/blob/v0.6.0/specification/sections/05-events.md) | Раздел versioned release: provider readiness, error, configuration changed и stale events. | Раздел experimental; это не бизнес-exposure и не delivery contract. |
| [Unleash Feature Toggles — immutable commit `341703978af887a364a9d872d9e3a906ae4dbbcb`, 21.08.2024](https://github.com/Unleash/unleash/blob/341703978af887a364a9d872d9e3a906ae4dbbcb/website/docs/reference/feature-toggles.mdx) | Неизменяемый первичный Git-снимок в пределах августа 2024: type и description, activation strategies по environment, disabled toggle evaluates false. | Не задаёт owner, expiry, наблюдение, метрики или cleanup process произвольной команды. |
| [Unleash Front-end API — immutable commit `4ae65d6d5e095d5c056ae63c60afcb09dd6cfe5b`, 24.05.2024](https://github.com/Unleash/unleash/blob/4ae65d6d5e095d5c056ae63c60afcb09dd6cfe5b/website/docs/reference/front-end-api.md) | Неизменяемый первичный Git-снимок до августа 2024: Front-end API, FRONTEND token, CORS и refresh interval с random offset для Unleash 4.18+. | Не является переносимой гарантией момента refresh, client-side evaluation или единой конфигурации у всех клиентов. |
Исторические меняющиеся документы закреплены либо датированным релизом `v0.6.0`, либо точным immutable Git commit. Архивные URL и старый домен документации Unleash из P78 удалены.
### Обязательная проверка ссылок
Перед выпуском выполнен обычный HTTPS GET для всех шести URL выше:
Команда запускалась без `-k`/`--insecure`, без TLS bypass, без заголовка авторизации, credentials или cookie jar. Результат для каждого URL — `200`; redirect ведёт на тот же публичный GitHub URL. Проверка выполнена 31.07.2026 для доступа к закреплённым историческим источникам, а не как утверждение об их текущем содержимом.
## Проход 1 — факты и техника
- У каждого материала первые два абзаца называют проблему и цену: долговая ветвистость release-флага; неясный authoritative evaluator и лишний context; false/100% без удаления runtime dependency.
- Цепочка в текстах одинаковая и проверяемая: «симптом → причина → проверка → действие». Это не рекламный нарратив и не runbook, притворяющийся production-инструментом.
-В mechanism-материале строго разделены evaluation, exposure candidate, render confirmation и domain effect. Provider events OpenFeature не выданы за бизнес-события. Ни одна статья не обещает exactly-once; при необходимости назван idempotency key на стороне потребителя.
- В practice-материале owner, expiry и deletion path честно названы командной policy, а не возможностью OpenFeature или Unleash.
- В field-материале 0/5/25/100, сигналы и сценарии — `fixed synthetic model`. Это не трафик, SLO, incident, пользовательские данные, интервью или production-метрики.
- Фикстура принимает только канонический fixed synthetic input. Она отвергает unknown keys, file/network/clock/telemetry/production markers, неверную версию и scope, разрежённые массивы, подменённые карточки/решения, циклический input/report и forged cleanup draft. Отдельная проверка доказывает, что некорректный input не меняет frozen fixed record.
## Проход 2 — редактирование и голос
Автор августа 2024 — системный практик: короткие технические абзацы, конкретный контекст и явная граница вывода. В каждом тексте есть постановка проблемы в первых двух абзацах, таблица решения, компактный воспроизводимый код или конфиг, ограничения и один следующий проверяемый шаг.
| Slug | Основное тело | Редакторский контроль |
| --- | ---: | --- |
| `editorial-2024-08-practice-feature-flags` | 9 586 знаков | Контракт флага проходит от owner и audience к наблюдению и removal; сигнал не назван доказательством эффекта. |
| `editorial-2024-08-mechanism-feature-flags` | 11 878 знаков | Граница server/client, cohort key и event semantics разобраны без магических гарантий provider. |
| `editorial-2024-08-field-feature-flags` | 12 045 знаков | Rollout, rollback и cleanup разведены; 100% и disabled явно не названы завершением удаления. |
Диапазон 5 000–15 000 знаков соблюдён для всех трёх тел. Английские термины оставлены только там, где они обозначают API, контракт или артефакт; рядом дано техническое значение, а не рекламная формулировка.
## Проход 3 — визуал и выпуск
Три SVG нарисованы вручную; в них нет `<script>`, `<foreignObject>`, `javascript:`, `data:image` или event handlers. После первого Sharp-render на 375 px длинные подписи cleanup/delivery были разбиты на короткие строки; затем сделан повторный визуальный просмотр всех трёх мобильных PNG.
Полная база не собиралась: это явно оставлено главному агенту после интеграции. P78 не staged, не committed и не pushed.
## Выпуск после трёх независимых проходов
1.**Факты и техника.** Редактор повторно сверил OpenFeature v0.6.0 release и raw tagged specification: typed evaluation, optional context, default value при abnormal execution, experimental context/events и их границы совпадают с текстом. Два Unleash immutable commits подтвердили environment strategies, false для disabled toggle, Front-end API, FRONTEND token, CORS и refresh с random offset. `node --check` и fixture — **PASS 35/35**.
2.**Редактура и голос.** В публичном field-материале служебное имя партии `P78` заменено на «учебная фикстура». Остальные три текста проверены на разные задачи: release contract, server/client boundary и cleanup. Объёмы после вычитки — **9 586**, **11 878**, **12 049** знаков; каждое решение начинает с наблюдаемой цены и заканчивается проверяемым действием.
3.**Визуал и выпуск.** Три SVG прошли XML/safety scan, повторный Sharp-render и ручной просмотр на 375 px: decision tree, delivery boundary и cleanup gate остаются читаемыми. После календарного подключения перед сентябрём `audit:articles` подтвердил для каждого slug 1 figure, 2 table и 1 code example; registry содержит **232** уникальные ревизии. `npm run build` завершился успешно: **374** статические страницы.
В выпуск включаются только пять файлов августа, обновления registry/production README и эта приёмка. Пользовательские изменения, черновики октября/ноября и исходный `articles.json` исключены.
<titleid="title">Синтетическая временная схема rollout, rollback и cleanup feature flag</title>
<descid="desc">Схема показывает синтетические этапы rollout ноль, пять, двадцать пять и сто процентов. При stop condition выполняется rollback к fallback. После стабильного финального варианта отдельный cleanup gate требует удалить ветки и конфигурацию.</desc>
<textx="70"y="183"fill="#89abc1"font-family="Arial, sans-serif"font-size="17"font-weight="700">ROLLOUT: один cohort key и заранее записанная stop condition</text>
<textx="102"y="434"fill="#86acc2"font-family="Arial, sans-serif"font-size="15">Схема не задаёт безопасный процент, окно наблюдения или production SLO.</text>
<titleid="title">Дерево решения для release feature flag</title>
<descid="desc">Схема показывает, что до создания release-флага нужно заполнить карточку контракта. Без карточки работа уходит на review. После карточки отдельно выбираются server-side protected decision, client-side presentation и cleanup path.</desc>
<titleid="title">Поток решения feature flag между сервером и клиентом</title>
<descid="desc">Схема отделяет защищенный серверный контекст, evaluation, ответ с presentation value и configuration version, от клиентского rendering и candidate exposure event. Пунктирная линия подчёркивает отсутствие exactly-once гарантии.</desc>
note:'Первичный versioned release OpenFeature, опубликованный 16 мая 2023 и уже доступный к августу 2024. В выпуск вошли provider events, initialization и shutdown; это не доказательство одинаковой реализации у каждого provider или exactly-once доставки какого-либо события.',
note:'Первичная спецификация typed evaluation: flag key, default value и optional evaluation context; при abnormal execution возвращается default value. Раздел помечен hardening и задаёт API-контракт, а не policy хранения секретов, TTL или жизненный цикл конкретного флага.',
note:'Первичная спецификация context: targeting key идентифицирует subject, а global, client и invocation context имеют порядок merge. Раздел experimental; он не обещает непрерывную консистентность когорты между сервисами, браузерами или версиями конфигурации.',
note:'Первичная experimental-спецификация provider events: readiness, error, configuration changed и stale. Она описывает обработчики состояния provider, но не является семантикой бизнес-exposure, не определяет delivery transport и не даёт exactly-once гарантию.',
note:'Первичный неизменяемый Git-снимок Unleash от 21 августа 2024: флаг имеет type и description, а activation strategies настраиваются по environment; disabled toggle в environment evaluates false. Этот commit не утверждает, что у произвольной команды есть owner, срок удаления, метрика или единая стратегия cleanup.',
}),
Object.freeze({
title:'Unleash: Front-end API access, immutable Git commit 4ae65d6 (24.05.2024)',
note:'Первичный неизменяемый Git-снимок Unleash от 24 мая 2024: отдельный front-end API, FRONTEND token, CORS boundary и refresh interval с random offset. Это конкретный механизм Unleash 4.18+, а не универсальная модель client-side evaluation или обещание моментальной смены всех клиентов.',
symptom:'a temporary release switch is treated as a harmless boolean and has no named removal date',
cause:'the delivery decision, owner, audience, observation boundary and cleanup work were never written as one contract',
check:'verify that one card names the safe fallback, one owner, one audience, one expiry review and one deletion path before a real toggle is created',
action:'create only a human review draft for the card; do not create, change or evaluate a real flag',
limitation:'the fixed card is a teaching record, not evidence of a real release, incident, audience or measurement result',
}),
}),
'fixed-server-decision-v1':Object.freeze({
label:'a protected eligibility decision stays on the server while the client receives only an already-decided presentation value',
invalidationQuestion:'what happens when the effective configuration version changes during an active client session',
}),
decision:Object.freeze({
code:'keep-protected-decision-server-side',
symptom:'the browser is asked to reconstruct a decision that depends on entitlement or another protected input',
cause:'decision ownership, cohort key and client payload are mixed; an exposure event is mistaken for proof that the user saw an effect',
check:'write the server input, the stable cohort key, the returned presentation value, the config version and the event delivery boundary separately',
action:'keep the protected rule at the server boundary and make event handling idempotent where the receiving system requires it; do not claim exactly-once',
limitation:'the fixed record does not call an SDK, store a context, send an event, observe a browser or establish cross-service consistency',
}),
}),
'fixed-rollout-cleanup-v1':Object.freeze({
label:'a staged release model with explicit stop conditions, rollback to the fallback and cleanup only after code evidence',
'// Не читаются flag store, файлы, сеть, часы, CI, telemetry, user data или production.',
'// Exposure boundary не означает exactly-once delivery и не доказывает effect.',
].join('\n');
constfixtureCommand=fixtureExample+'\n\nnode web/scripts/upgrade-2024-08.mjs --verify-fixture\n\n# PASS подтверждает только согласованность fixed synthetic records и отрицательных веток.';
constpractice=revision({
slug:'editorial-2024-08-practice-feature-flags',
title:'Фича-флаг как контракт выпуска: как не оставить переключатель навсегда',
excerpt:'Практический контракт release-флага: владелец, аудитория, срок пересмотра, сигналы, fallback и удаление — до того, как временный boolean попадёт в код.',
readingMinutes:13,
},[
p('В релизе появляется обычный boolean: новый checkout включается только для части аудитории. Через неделю задача считается законченной, но у флага нет владельца, даты пересмотра и ответа, что именно будет удалено после выбора варианта. Через месяц он уже участвует в трёх условиях: на сервере, в клиентском интерфейсе и в тестовой матрице. Цена ошибки не в одной строке конфигурации. Следующая доработка должна одновременно помнить старый путь, новый путь и все исключения, которые когда-то были нужны только для выпуска.'),
p('Вторая ошибка выглядит осторожной: после сбоя флаг выключают и считают риск снятым. Выключение уменьшает аудиторию нового пути, но не удаляет ветки, не отменяет различие данных и не показывает, какая проверка позволила выбрать итоговое поведение. Если в карточке нет safe fallback и плана удаления, команда получает не контроль релиза, а накопленный долг с хорошим названием. Поэтому release-флаг удобнее рассматривать как короткий контракт выпуска, а не как свободный переключатель.'),
h2('Симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> В pull request появляется фраза «добавим временный флаг», но нет человека, который потом удалит обе ветки.',
'<strong>Причина.</strong> Boolean хранит только значение, а решение выпуска включает аудиторию, fallback, наблюдение, срок пересмотра и условие удаления.',
'<strong>Проверка.</strong> До реального создания флага заполните одну карточку: owner, класс, аудитория, безопасное значение, срок review, сигналы, stop condition и путь удаления.',
'<strong>Действие.</strong> Если карточку нельзя заполнить, не расширяйте код условием. Сначала проведите короткий review выпуска: может оказаться, что нужен другой механизм, а не ещё один флаг.',
]),
h2('Что именно должен обещать release-флаг'),
p('Контракт не обязан быть большим. Ему достаточно назвать один вопрос, на который отвечает переключатель. Например: можно ли показать новую форму checkout выбранной синтетической когорте, сохраняя прежнюю форму как fallback. Вопрос не должен звучать как «включить новый checkout вообще»: в нём нет аудитории, границы и критерия остановки. Чем шире формулировка, тем легче превратить временный флаг в постоянный режим системы.'),
p('У карточки есть owner, но это не декоративное поле. Owner отвечает за следующий review: продлить срок с явной причиной, выбрать итоговый вариант или инициировать cleanup. Аудитория тоже описывается технически: какой ключ делает когорту стабильной в пределах одной зафиксированной конфигурации, и какие входы не должны покидать сервер. Для простого UI-флага можно вернуть клиенту уже вычисленное presentation value. Для entitlement, цены, права или другого защищённого ввода решение остаётся на серверной границе.'),
table('Минимальная карточка release-флага',['Поле','Что фиксируем','Чего поле не доказывает'],[
['Ключ и класс','один ключ, release-класс и один вопрос выпуска','что флаг подходит для эксперимента, миграции данных или permanent policy'],
['Owner','человек или роль, принимающие review срока и cleanup','что владелец уже проверил каждую зависимость нового пути'],
['Аудитория','cohort key, environment и явно разрешённый контекст','что все сервисы мгновенно увидят одну и ту же версию конфигурации'],
['Fallback','какой старый путь должен остаться выполнимым при остановке','что rollback исправит данные, уже записанные новым путём'],
['Срок review','дата или событие, когда нужен новый выбор','автоматическое удаление кода без human review'],
['Сигналы','какие типы сигналов допустимы для решения: outcome, ошибка рендера, контролируемая проверка','причину любого отклонения или измеренный успех в production'],
figure('/assets/editorial/2024/feature-flags-2024-decision-tree.svg','Дерево решения release-флага: сначала проверяются owner, аудитория, fallback и срок review; затем выделяются server-side protected decision, client-side presentation и отдельный путь cleanup. Схема показывает контракт, а не состояние реального feature-flag сервиса.','Флаг появляется только после заполнения границы решения. Ветка «нет карточки» ведёт к review, а не к новому boolean. Диаграмма не измеряет rollout, ошибки или использование флага.'),
h2('Сигналы нужны для решения, а не для декорации'),
p('У сигнала есть вопрос и граница. Событие evaluation говорит, что код попытался вычислить вариант; оно не доказывает, что пользователь увидел интерфейс и тем более не доказывает полезный эффект. Ошибка рендера может показать, что presentation-path не собрался, но не объясняет, какой внешний сервис вызвал сбой. Контролируемая функциональная проверка может подтвердить один ожидаемый маршрут, но не заменяет поток реальных пользовательских данных. В карточке лучше писать тип сигнала и его ограничение рядом.'),
table('Сигнал и допустимый вывод',['Сигнал','На какой вопрос отвечает','Не допускает такой вывод','Следующее действие'],[
['Evaluation outcome','какой вариант вернул evaluator для согласованного context','пользователь обязательно увидел эффект; event доставлен ровно один раз','сверить key, version и context boundary'],
['Клиентская ошибка','сломался ли контролируемый presentation path','корень проблемы находится в самом флаге','разделить ошибку интерфейса, API и данных до изменения audience'],
['Проверка fallback','старый путь всё ещё способен выполнить объявленный контракт','новый путь безопасен при любой нагрузке','оставить fallback до отдельного cleanup review'],
['Изменение конфигурации','появилась новая effective configuration version','все клиенты синхронно сменили когорту','определить cache, refresh и правила для активной сессии'],
]),
h2('Короткий объект лучше списка обещаний'),
p('Ниже не конфигурация Unleash или другого сервиса. Это маленькая запись для review и одновременно пример границы: она не содержит URL, credential, реальную аудиторию или production-метрику. В ней есть только то, что нужно обсудить до создания настоящего флага. Значение срока не запускает timer; signals не включают telemetry; deletion path не удаляет файл. Так карточка остаётся документом решения, а не скрытым оператором инфраструктуры.'),
'// Учебная карточка: она не создаёт флаг и не считывает реальную конфигурацию.',
].join('\n')),
h2('Как срок превращается в техническую работу'),
p('Срок review не означает, что в указанную дату робот должен удалить флаг. Его задача другая: не дать boolean исчезнуть из внимания. В день review owner выбирает одно из трёх действий. Первое — итоговый вариант ещё не выбран: ограничить аудиторию или продлить контракт с новой причиной. Второе — выбран fallback: вернуть аудиторию к нулю и разобраться, какие побочные данные или зависимости остались. Третье — выбран новый путь: заморозить вариант, открыть cleanup и не добавлять новые rules в старый флаг.'),
p('Важно не называть продление нейтральным. Оно увеличивает стоимость тестирования и увеличивает число вариантов, которые должен понимать любой следующий разработчик. Поэтому к продлению полезно приложить тот же набор фактов, что к первому включению: текущая аудитория, причина задержки, владелец, безопасное значение, новый review и список branch, которые всё ещё существуют. Если этих фактов нет, срок продлевают не из-за неопределённости системы, а из-за отсутствия решения.'),
h2('Граница исторических источников'),
p('К августу 2024 OpenFeature v0.6.0 уже описывал typed evaluation, optional context и provider events, но sections имели статусы hardening или experimental. Неизменяемые commits Unleash от 21 августа и 24 мая 2024 подтверждают отдельные configuration, environment и front-end API boundaries, включая token, CORS и refresh semantics конкретного продукта. Ни один источник не задаёт универсальные поля owner, expiry или deletion path. Это сознательно добавленная командная policy, а не свойство SDK.'),
h2('Порядок внедрения без лишней платформы'),
ol([
'<strong>Ограничьте вопрос.</strong> Для одного нового изменения напишите old path, candidate path и безопасный fallback. Не объединяйте release, pricing policy и эксперимент в одном ключе.',
'<strong>Назначьте owner и review.</strong> Owner получает не право «забыть флаг», а обязанность вынести на review итог, продление или cleanup.',
'<strong>Определите audience boundary.</strong> Зафиксируйте environment, cohort key и чувствительные входы. Если решение зависит от закрытых данных, клиент получает результат, а не правило.',
'<strong>Запишите наблюдение.</strong> Для каждого сигнала назовите, что он подтверждает и чего не подтверждает. Не используйте event как обещание exactly-once.',
'<strong>Проверьте fallback.</strong> До расширения аудитории убедитесь, что старый путь ещё выполним в разрешённой среде и что возврат не маскирует изменение данных.',
'<strong>Откройте cleanup заранее.</strong> Когда новый вариант выбран, запрещайте новые rules в старом флаге и планируйте удаление кода, config, tests и документации отдельным change.',
]),
h2('Ограничения и следующий проверяемый шаг'),
p('Карточка не заменяет approval, threat model, миграцию схемы, canary-policy или SLA. Она не выбирает безопасный процент rollout и не говорит, какие поля context допустимы с точки зрения персональных данных. В частности, immutable commit Front-end API Unleash от 24 мая 2024 описывает один продукт и его refresh behavior; переносить его interval, token model или нагрузочную границу в другой provider нельзя. Context OpenFeature описывает форму API, но не гарантирует, что все ваши сервисы используют одинаковый ключ и одинаковое время обновления.'),
p('Следующий проверяемый шаг — взять один существующий release-флаг и выписать для него семь строк из первой таблицы. Затем найти ровно одну недостающую: owner, fallback, audience, expiry, signal или deletion path. Не исправляйте всё сразу. Сначала добавьте запись и назначьте review. Ожидаемый результат маленький, но проверяемый: следующий читатель флага сможет назвать, зачем он существует, кто примет решение и какие ветки будут удалены после этого решения.'),
]);
constmechanism=revision({
slug:'editorial-2024-08-mechanism-feature-flags',
title:'Где принимать решение флага: server, client и журнал exposure',
excerpt:'Как выбрать точку вычисления флага, сохранить когортный ключ и отделить evaluation от exposure без мифа о exactly-once событии.',
readingMinutes:14,
},[
p('Флаг меняет не только интерфейс. Он решает, где живёт правило: на сервере, в браузере или одновременно в двух местах. Ошибка обычно проявляется после первого успешного rollout. Клиент получил достаточно входов, чтобы сам вычислить eligibility; сервер вернул один вариант, а браузер при следующем запросе — другой; аналитика назвала один event exposure, хотя пользователь не видел результат из-за ошибки рендера. Цена — не только утечка лишнего контекста. Команда теряет возможность объяснить, почему конкретный запрос увидел именно этот путь.'),
p('Противоположная крайность тоже плоха: каждый косметический элемент требует round trip к серверу, хотя правило зависит только от уже публичной настройки приложения. Здесь стоимость — задержка и лишняя связность. Вопрос звучит не «server или client лучше». Он звучит так: какие входы защищены, какой компонент владеет окончательным решением, какой ключ удерживает когорту и что именно означает запись exposure. Без этих четырёх ответов флаг превращается в распределённое угадывание.'),
h2('Симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Для одного account UI иногда показывает новый вариант, а API продолжает выполнять старую ветку, либо браузер получил entitlement, который ему не нужен.',
'<strong>Причина.</strong> Входы, evaluator, cohort key, config version и event semantics распределены по разным слоям без единого contract.',
'<strong>Проверка.</strong> Для каждого флага выпишите protected input, public presentation input, authoritative evaluator, stable targeting key, effective config version и тип события.',
'<strong>Действие.</strong> Оставьте решение там, где доступен минимальный набор допустимых входов. Клиенту передавайте только результат и данные, нужные для отображения; exposure делайте идемпотентным на стороне потребителя, не обещая exactly-once.',
]),
h2('Три слоя решения'),
p('Первый слой — input. Здесь важно различить публичный параметр интерфейса и защищённый факт. Локаль, объявленная тема или заранее опубликованный вариант текста могут быть client-safe, если они действительно уже доступны клиенту. Право доступа, индивидуальная скидка, fraud signal, внутренний account state и правило, раскрывающее их, не должны становиться материалом для browser evaluator только ради удобства rollout. Если клиенту нужен ответ «можно ли показать кнопку», сервер может вернуть boolean и version, не раскрывая причину.'),
p('Второй слой — evaluator. Он отвечает за вычисление варианта для конкретного context. OpenFeature v0.6.0 описывает typed evaluation с key, default value и optional context; это полезная форма, но не указание, где запускать provider. Источник позволяет разделить contract вызова и implementation: server-side evaluator может иметь защищённый context, client-side evaluator — только разрешённый поднабор. Решение о placement остаётся архитектурным: оно зависит от данных, latency, availability и доверенной границы.'),
p('Третий слой — presentation. Результат evaluation ещё не равен показанному эффекту. Между ними может быть network response, client cache, rendering, feature branch и пользовательское действие. Поэтому слово exposure нужно договорить. В этой статье exposure — попытка зафиксировать, что evaluator вернул конкретный вариант для заданного context и configuration version. Такая запись не доказывает показ интерфейса, не измеряет результат и не обязана быть доставлена ровно один раз.'),
table('Точка вычисления и её цена',['Вариант','Допустимые входы','Сильная сторона','Главный риск','Проверка перед выбором'],[
['Server decision','protected entitlement, pricing rule, account state, server-only policy','клиент получает минимум данных и готовый verdict','лишний сетевой путь и рассинхрон при повторной client evaluation','вернуть только presentation value и config version; не дублировать правило в браузере'],
['Split decision','server считает eligibility, client выбирает только presentation within returned contract','можно отделить policy от UI','два слоя могут незаметно менять один и тот же смысл','назвать single source of truth и запретить клиенту переоценивать protected rule'],
['Два независимых evaluator','разные наборы данных без согласованной версии','нет технического преимущества само по себе','один subject получает разные варианты и спор о виновнике','не использовать без явного consistency contract'],
]),
figure('/assets/editorial/2024/feature-flags-2024-rollout-timeline.svg','Схема потока решения: server получает защищённый context, вычисляет eligibility и возвращает клиенту presentation value вместе с configuration version; client фиксирует отдельный exposure candidate. Пунктиром показано, что доставку события нельзя считать exactly-once.','Диаграмма отделяет decision, rendering и event delivery. Она не показывает SDK traffic, реальную когорту, latency или результаты аналитики.'),
h2('Когорта начинается с ключа, а не с процента'),
p('Процентный rollout устойчив только относительно выбранного ключа и версии правила. Если сервер выбирает cohort по account ID, а браузер — по anonymous ID, один человек может попасть в разные варианты на соседних экранах. Если key меняется после логина, нужен отдельный переход: какая версия сохраняется в сессии, как объединяются pre-login и post-login события, какой вариант имеет приоритет. Молчаливо считать, что hash сам решит эту задачу, нельзя.'),
p('OpenFeature context v0.6.0 называет targeting key идентификатором subject и допускает custom fields; provider может требовать его для fractional evaluation. Это полезное ограничение: ключ нужен сделать явным. Но спецификация не обещает distributed snapshot. Когда effective configuration version меняется, когорту может корректно пересчитать следующий запрос. Система должна решить, допустимо ли это во время активной сессии, или нужна pinning policy. Такой выбор лучше записать рядом с флагом, чем искать его в cache invalidation после жалобы.'),
h2('Минимальный воспроизводимый пример'),
p('Следующий пример не подключает provider и не отправляет event. Он берёт только фиксированную synthetic карточку из этого overlay. Она помогает проверить форму решения: protected input остаётся за server boundary, client получает boolean и version, а exposure boundary прямо содержит запрет на exactly-once обещание. Никакого account, токена, endpoint, event broker или production config здесь нет.'),
" throw new Error('unexpected teaching decision');",
'}',
'',
'console.log(report.flagCard.clientPayload);',
"// already-decided-boolean-and-config-version",
'console.log(draft.actions[2]);',
"// treat-exposure-delivery-as-not-exactly-once",
].join('\n')),
p('Эта fixture строит report только из object literal, заранее записанного в модуле. Любой field, похожий на flag store, URL, clock, CI, telemetry или production marker, отклоняется до анализа. Если подменить decision, cohort key, config version question или разрежить массив stages, plan не будет принят. Такой тест не проверяет реальный SDK. Он защищает пример от ложного вывода, что запись document уже стала вызовом production-системы.'),
h2('Evaluation, exposure и effect — разные факты'),
p('Evaluation — evaluator получил key и context и вернул вариант или default. Exposure candidate — приложение подготовило запись, что этот вариант был вычислен в определённой версии конфигурации. Rendered exposure — клиент действительно дошёл до точки показа; он всё ещё не равен успешному действию. Business effect требует отдельного доменного события и отдельной методологии сравнения. Склеивать эти уровни в одно слово «показали фичу» опасно: при сбое невозможно понять, что именно не произошло.'),
p('У event delivery есть собственная семантика. Provider events OpenFeature v0.6.0 относятся к готовности, ошибке, изменению configuration или stale state provider. Они не определяют бизнес-event ingestion. Даже если ваш transport повторяет сообщение при сбое, это не превращается в global exactly-once: retry, timeout, consumer crash и переигранная сессия остаются отдельными случаями. Если downstream требует дедупликацию, он должен иметь идемпотентный key и объяснение окна хранения. Текст события должен называть attempt или delivered record, а не «единственный факт показа».'),
table('Факт, который можно записать, и факт, который нельзя подменить',['Запись','Что содержит','Чего не обещает'],[
['Evaluation record','flag key, returned variant, context boundary, effective config version','что UI успешно отрендерился и событие доставлено'],
['Exposure candidate','idempotency key, request or session boundary, chosen variant','exactly-once delivery, уникального пользователя или полезного действия'],
['Domain event','отдельное действие после показа и свой business contract','что изменение вызвано только флагом без методики контроля'],
]),
h2('Изменение конфигурации не обязано быть мгновенным'),
p('Immutable commit Front-end API Unleash от 24 мая 2024 описывает отдельный продуктовый путь: FRONTEND token, CORS boundary и refresh interval с random offset. Из этого следует практический вопрос, а не универсальный тайминг: когда именно ваш клиент узнаёт о новой версии и как он ведёт себя до этого. Server evaluator может получить configuration раньше браузера; Edge или proxy — иметь другой cache; локальный provider — другой lifecycle. В production contract стоит говорить «version returned by this evaluator», а не «все пользователи уже на новом проценте».'),
p('Когда нужны несколько evaluator, полезнее передавать result than rule. Server возвращает selected variant и effective config version. Client может кешировать presentation в допустимой границе, но не должен заново применять hidden targeting. Если UI обязан обновиться после config change, отдельно определите event, refresh, invalidation и user-visible transition. Это делает некрасивый случай видимым: пользователь может увидеть старый вариант до следующего refresh, и это не ошибка именно флага, пока contract говорит, что такое поведение разрешено.'),
h2('Порядок проектирования'),
ol([
'<strong>Классифицируйте inputs.</strong> Отделите protected facts от client-safe presentation fields. Список должен быть короче реального context, а не равен ему.',
'<strong>Выберите authoritative evaluator.</strong> Один слой владеет eligibility. Второй не пересчитывает то же правило с другим key или другой config version.',
'<strong>Зафиксируйте cohort key.</strong> Назовите subject, переход login/logout, version boundary и поведение активной сессии.',
'<strong>Опишите payload.</strong> В client response положите только verdict, variant и нужную version; не передавайте скрытый rule ради удобной отладки.',
'<strong>Разделите события.</strong> Evaluation, exposure candidate, render и domain effect должны иметь разные названия, schema и ограничения.',
'<strong>Проверьте delivery честно.</strong> Если нужен dedupe, определите idempotency key и consumer behavior. Не заменяйте это словом exactly-once.',
'<strong>Проведите один controlled check.</strong> В разрешённой среде сравните один subject, один key и одну config version на двух границах. Не меняйте одновременно key, cache и rollout percent.',
]),
h2('Ограничения и следующий проверяемый шаг'),
p('Этот механизм не выбирает SDK, cache TTL, payload schema, event transport или срок хранения дедупликационных ключей. Он не доказывает, что client token действительно ограничен, CORS правильно настроен или provider последовательно выдаёт один вариант. OpenFeature разделы про context и events на v0.6.0 были experimental; их нельзя читать как SLA. Immutable commit Front-end API Unleash от 24 мая 2024 описывает Unleash 4.18+ и не даёт переносимой гарантии для другой платформы.'),
p('Следующий проверяемый шаг — выбрать один существующий флаг, зависящий от account-level данных, и заполнить таблицу из шести строк: protected input, public payload, evaluator, cohort key, config version and event type. Затем найдите одно дублирование правила между server и client. Уберите дублирование или запишите контракт, почему оно необходимо. Ожидаемый результат — конкретный: по одному event и одному response можно объяснить, кто принял решение и чего это событие не доказывает.'),
]);
constfield=revision({
slug:'editorial-2024-08-field-feature-flags',
title:'Выключить флаг — не значит удалить риск: rollout, rollback и cleanup',
excerpt:'Полевой маршрут для gradual rollout: synthetic stages, stop condition, fallback и отдельный cleanup gate, который удаляет ветки и конфигурацию после выбора итогового поведения.',
readingMinutes:13,
},[
p('После неудачного шага rollout команда выключает флаг. Новый путь перестаёт получать аудиторию, тревога стихает, change закрывают. Но проблема остаётся: через два релиза тот же код всё ещё хранит обе ветки, config содержит старый ключ, тесты знают два ответа, а developer не уверен, какой из них должен быть единственным. Риск не исчез: он просто перестал проявляться у пользователей. Любая следующая доработка может снова активировать старый путь случайной конфигурацией или оставить несовместимое состояние в коде.'),
p('Другой частый сценарий обратный. Rollout дошёл до 100 процентов, и это называют завершением. Но 100 процентов означает лишь, что правило сейчас выбирает candidate для объявленной аудитории. Это не доказательство, что fallback больше не нужен, что данные совместимы или что безопасно удалить ключ. Пока не зафиксирован final variant, не собраны разрешённые evidence и не удалены runtime branches, флаг продолжает расширять поверхность ошибки. Поэтому rollout, rollback и cleanup нужно держать как три разные операции.'),
h2('Симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Флаг выключен или включён на 100 процентов, но в коде остаются две ветки и никто не может назвать дату удаления.',
'<strong>Причина.</strong> Аудитория, fallback, наблюдение и cleanup смешаны в одной операции enable/disable.',
'<strong>Проверка.</strong> Для каждого stage запишите cohort key, observation question, stop condition, fallback action и отдельно cleanup gate с доказательствами удаления.',
'<strong>Действие.</strong> При stop condition сначала верните аудиторию к fallback. После выбора final variant начните новый change: удалить branches, config references, флаговые tests и документацию, сохранив обычную behaviour-проверку.',
]),
h2('Четыре состояния вместо одной шкалы процента'),
p('Процент rollout — только один параметр delivery. Он не сообщает, применился ли новый путь к конкретной сессии, пришёл ли refresh к клиенту, завершился ли запрос и безопасно ли удалять старый код. Поэтому у шага должны быть как минимум пять полей: stage, cohort key, окно наблюдения, stop condition и действие на остановке. Числа 0, 5, 25 и 100 ниже — не рекомендованная лестница и не данные реальной команды. Это fixed synthetic объект fixture, удобный для обсуждения переходов.'),
table('Учебная лестница rollout и действия',['Synthetic stage','Допустимый вопрос','Stop condition','Немедленное действие','Что остаётся открытым'],[
['0%','fallback path существует и его контракт ещё можно проверить','fallback не выполняет согласованный smoke scenario','не начинать rollout; вернуть change в design','все candidate branches и data assumptions'],
['5%','одна стабильная synthetic cohort получает candidate согласно declared key','согласованный synthetic signal нарушает заранее записанную границу','вернуть audience к 0%, сохранить change record','root cause и решение о повторном запуске'],
['25%','candidate и fallback можно сопоставить в одной разрешённой модели наблюдения','signal не интерпретируется или config version неизвестна','остановить увеличение аудитории, не менять одновременно rule и код','качество evidence и consistency boundary'],
['100%','вся объявленная аудитория получает selected variant в текущей config version','final variant ещё не утверждён либо fallback нужен для recovery','не удалять флаг; открыть final decision review','cleanup proof и сохранность данных'],
['Cleanup gate','code и config больше не требуют alternate branch','найдена хотя бы одна runtime reference или необходимый fallback','оставить key и вернуть cleanup в доработку','новый independent release удаления'],
]),
figure('/assets/editorial/2024/feature-flags-2024-cleanup-gate.svg','Временная схема отделяет rollout 0–5–25–100 процентов от rollback к fallback и от cleanup gate. После 100 процентов показан отдельный review, затем удаление веток, конфигурации и флаговых tests. Проценты — fixed synthetic model, не реальные показатели.','Выключение флага ведёт к mitigation и расследованию. Cleanup начинается только после выбора итогового поведения и проверки, что alternate path действительно удалён.'),
h2('Rollback сначала возвращает поведение, а не переписывает историю'),
p('Rollback нужен для короткого и понятного возврата к fallback. В момент stop condition не надо одновременно менять процент, evaluator, data migration и код. Иначе команда теряет причинную связь: неизвестно, что именно изменило наблюдение. Минимальная операция — вернуть объявленную аудиторию к нулю или к заранее выбранному safe value, оставить старый путь выполнимым и записать effective configuration version, на которой остановились. Этот action не лечит данные и не удаляет риск. Он только прекращает дальнейшее расширение candidate.'),
p('Если новый путь успел записать несовместимое состояние, rollback должен опираться на отдельный migration contract. Фича-флаг не заменяет backward compatibility. Нельзя обещать, что достаточно поставить false, если API response, database schema или side effect уже сменились. До первого rollout полезно спросить: может ли fallback прочитать состояние, созданное candidate, и какая команда владеет исправлением, если ответ нет. Если ответ не известен, это blocker дизайна, а не причина ускорить rollout маленьким процентом.'),
h2('Cleanup начинается с решения о единственном варианте'),
p('Флаг можно удалять только после того, как owner зафиксировал final variant. Это кажется очевидным, но без такого шага cleanup превращается в спор «оставим на всякий случай». Сначала замораживают правило: больше не добавляют conditions, segments, variants или новые client checks. Затем выносят отдельный change с областью удаления. В нём есть source files, server branches, client branches, configuration references, tests, docs и migration notes. Удалять всё одним глобальным search-and-replace рискованно: похожий ключ может быть частью другого текста, а feature flag может иметь несколько evaluator.'),
p('Проверка cleanup состоит из негативных утверждений. Runtime code больше не должен ветвиться по ключу. Конфигурация больше не должна содержать key, targeting rule или token permission, созданные только для него. Tests больше не должны тестировать два варианта, но обязаны сохранять проверку выбранного итогового поведения. Документация не должна приглашать новую команду включить уже отсутствующий путь. Если какая-то reference нужна для reversible migration, это не «почти удалено»: флаг всё ещё имеет ownership и review.'),
table('Cleanup gate: что ищем до удаления configuration',['Область','Проверяемый артефакт','Причина отказа cleanup','Действие'],[
['Server code','нет evaluator call и alternate branch для ключа','защищённое решение всё ещё зависит от flag result','удалить branch или разделить change на migration и cleanup'],
['Client code','нет presentation toggle, stale cache key или exposure schema, привязанной только к флагу','UI может снова трактовать старый result','сохранить только итоговый UI contract и проверить fallback assumptions'],
['Flag configuration','нет targeting rules, variants, environment overrides и токенов, нужных только для ключа','код ещё ожидает result либо нужен rollback','не удалять configuration до устранения references'],
['Tests','остался test итогового поведения и удалены flag-specific forks','test matrix по-прежнему требует оба пути','переписать test вокруг business contract, не вокруг boolean'],
['Документация','карточка выпуска закрыта решением и ссылкой на cleanup change','следующая команда не понимает, почему key исчез','оставить короткую decision запись без инструкции вернуть флаг'],
]),
h2('Воспроизводимая модель не притворяется production-runbook'),
p('Учебная фикстура хранит stages 0, 5, 25 и 100 как плотный массив fixed synthetic object. Она не читает dashboard, feature store, Git, CI, event broker или clock. Поэтому прогон может проверить только дисциплину модели: stage имеет rollback, cleanup gate требует удалённых веток и config, а input с URL, реальным ключом, телеметрией или production marker отвергается. Это полезнее, чем код, который выглядит как оператор и молча скрывает доступ к реальному флагу.'),
code(fixtureCommand),
p('В case <code>fixed-rollout-cleanup-v1</code> arrays stagesPercent, requiredSignals, rollback и cleanupGate намеренно фиксированы. Если заменить stages на разрежённый массив, подменить stop action или добавить output от настоящего API, plan отказывает. Если сделать decision cyclic, проверка завершается отказом, а не исключением. PASS не означает, что 5 процентов безопасны или cleanup завершён. Он означает, что учебный объект не потерял свою границу и не стал каналом управления production.'),
h2('Как пройти от rollout к удалению'),
ol([
'<strong>До первого включения.</strong> Проверьте fallback contract и data compatibility. Зафиксируйте owner, cohort key, stop condition и один разрешённый source evidence.',
'<strong>На каждом stage.</strong> Меняйте только audience или только rule в одном change. Записывайте effective config version и вопрос наблюдения; не выдавайте случайный event за эффект.',
'<strong>При остановке.</strong> Сначала верните audience к fallback. Не удаляйте code, пока не известно, что candidate больше не нужен для diagnosis или migration.',
'<strong>После 100%.</strong> Проведите final decision review. «Все получают candidate» не является решением удалить fallback.',
'<strong>Откройте cleanup change.</strong> Заморозьте flag policy, перечислите references и разделите удаление кода, config, tests и документации на проверяемые шаги.',
'<strong>Проверьте отрицательные условия.</strong> Search, unit/integration tests и config review должны показать отсутствие runtime dependence. Затем deploy cleanup как отдельное изменение с обычным rollback plan.',
'<strong>Закройте карточку.</strong> Сохраните final variant, причину удаления, owner и ссылку на cleanup evidence. Не сохраняйте флаг «для памяти»: память хранится в decision record, не в выполняемой ветке.',
]),
h2('Почему disabled flag не является доказательством cleanup'),
p('Immutable commit Feature Toggles Unleash от 21 августа 2024 говорит, что disabled toggle в environment evaluates false. Это ценная механическая гарантия конкретного продукта: включённая стратегия не будет случайно выбрать true, пока флаг disabled в этом environment. Но из неё не следует, что server code перестал вычислять флаг, клиент перестал знать ключ, тесты перестали держать две ветки или секреты/permissions уже удалены. Даже false evaluation остаётся dependency, если приложение продолжает спрашивать ответ.'),
p('Именно поэтому policy cleanup намеренно сильнее disablement. Сначала нужно доказать, что выбранный final behavior существует без evaluator. Потом убрать configuration, чтобы будущая случайная смена не могла оживить старый путь. И только затем закроется долг. В некоторых системах порядок config/code может отличаться из-за deployment topology; это надо явно описать в change. Универсального «сначала delete flag» нет. Есть только правило: ни один шаг не должен оставлять исполняемый code без нужного ему contract.'),
h2('Ограничения и следующий проверяемый шаг'),
p('Материал не задаёт rollout percent, SLO, error budget, retention периода, способ миграции или команду для feature platform. Синтетическая шкала не описывает трафик, conversion, latency, incident или real cohort. Источники OpenFeature и Unleash задают узкие API/configuration facts, но не дают готового cleanup process вашей организации. Отдельно нужно проверить provider semantics, token scopes, cache, legal boundary контекста и совместимость данных конкретного изменения.'),
p('Следующий проверяемый шаг — выберите один отключённый флаг и проведите короткий cleanup gate из второй таблицы. Не удаляйте key сразу. Сначала найдите одну живую reference: server branch, client branch, config, test или документ. Назначьте её owner и откройте отдельный change удаления. Ожидаемый результат: флаг перестаёт быть тёмным углом репозитория, потому что есть одно решение, один список оставшихся references и путь к единственному поведению.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.