From 9f176036f1f86e4106adc3b2e7651db461b85835 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 09:58:58 +0000 Subject: [PATCH] architect(ET): auto-commit from architect run_id=362 --- .../ADR-001-tracker-plane-status-and-link.md | 224 ++++++++++++++++++ .../ORCH-067/07-infra-requirements.md | 46 ++++ .../ORCH-067/08-data-requirements.md | 35 +++ docs/work-items/ORCH-067/10-tech-risks.md | 21 ++ 4 files changed, 326 insertions(+) create mode 100644 docs/work-items/ORCH-067/06-adr/ADR-001-tracker-plane-status-and-link.md create mode 100644 docs/work-items/ORCH-067/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-067/08-data-requirements.md create mode 100644 docs/work-items/ORCH-067/10-tech-risks.md diff --git a/docs/work-items/ORCH-067/06-adr/ADR-001-tracker-plane-status-and-link.md b/docs/work-items/ORCH-067/06-adr/ADR-001-tracker-plane-status-and-link.md new file mode 100644 index 0000000..fe5d7bd --- /dev/null +++ b/docs/work-items/ORCH-067/06-adr/ADR-001-tracker-plane-status-and-link.md @@ -0,0 +1,224 @@ +# ADR-001: Источник Plane-статуса для live-карточки и кликабельный номер задачи + +- **Статус:** Proposed +- **Дата:** 2026-06-08 +- **Задача:** ORCH-067 +- **Слой:** B (индикация), НЕ слой A (машина стадий) — см. CLAUDE.md / ORCH-066 +- **Связи:** ORCH-066 (статусная модель Plane, `_PLANE_NAME_TO_KEY` / `_STAGE_TO_STATE_KEY`), + ORCH-042 (live-tracker, режимы `edit`/`bump`), ORCH-017 (`_build_plane_issue_link`, + `plane_web_url`/`plane_workspace_slug`, loopback-guard), ORCH-059 (Confirm Deploy), + ORCH-060 (`fetch_issue_state`), ORCH-010 (`get_project_states` per-project + кэш), + adr-0001 (реестр проектов), adr-0010 (post-deploy monitor). + +## Контекст + +ТЗ ORCH-067 (`02-trz.md`) фиксирует объём изменений; данный ADR закрывает развилки, +явно отданные архитектору метками `[ARCH]`: + +1. **Источник «истинного» Plane-статуса для веток, не выводимых из `tasks.stage`** + (Needs Input, Blocked, Rejected, Cancelled, Deploying, Monitoring after Deploy), + при **запрете менять схему БД** (нельзя добавить колонку-флаг). TZ §2.2 предлагает + два варианта: (а) best-effort чтение живого Plane-статуса с fail-safe; + (б) только stage-выводимые статусы. +2. **Способ доступа к `plane_issue_id`/`project_id`** в каждой точке `send_telegram`, + где есть только `work_item_id` (требование 4), оставаясь fail-safe. +3. Смена дефолта `tracker_mode` (`edit` → `bump`) для общего инстанса. + +### Ключевая находка анализа (определяет развилку 1) + +Когда аналитик задаёт вопросы, `stage_engine.start_pipeline` при наличии +`01-questions.md` вызывает `set_issue_needs_input(work_item_id)` (Plane → Needs Input), +но **DB-стадия остаётся `analysis`**, а BRD-часы (`brd_review_started_at`) **не +запускаются** (они стартуют позже, в `notify_approve_requested`, когда BRD готов). +Следовательно состояния **`Analysis` (аналитик работает)** и **`❓ Needs Input` +(аналитик ждёт ответа)** **неразличимы** по offline-данным БД (`stage` + brd-clock). +Единственный авторитетный источник этого различия — **живой Plane-статус**, который +оркестратор сам выставил через `set_issue_needs_input`. + +То же касается `Deploying` / `Monitoring after Deploy`: на стадии `deploy`/`done` +конкретная фаза self-deploy видна только в Plane (ORCH-059/ORCH-066), не в `tasks.stage`. + +Вывод: чисто-offline вариант (б) **не покрывает обязательный по DoD `❓ Needs Input`** +(AC-8). Нужен гибрид. + +## Решение + +### Р-1. Гибрид: offline-first ядро + best-effort live-overlay + +Статус карточки строится в два слоя; **offline-ядро авторитетно и всегда работает без +сети**, live-overlay лишь дорисовывает ветки, неотличимые offline. + +**Слой 1 — чистая offline-функция `plane_status_label(task_row) -> str`** в +`src/notifications.py`. Детерминированная, **никогда не бросает**, **никогда не ходит в +сеть**. Маппинг (имена статусов — финальные из ORCH-066 `_PLANE_NAME_TO_KEY`): + +| Источник (DB) | Метка карточки | +|---|---| +| `stage == "created"` | `To Analyse` | +| `stage == "analysis"`, brd-clock не запущен | `Analysis` | +| `stage == "analysis"`, `brd_review_started_at` есть, `brd_review_ended_at` пуст | `⏸️ In Review — ожидание согласования BRD` | +| `stage == "architecture"` | `Architecture` | +| `stage == "development"` | `Development` | +| `stage == "review"` | `Code-Review` | +| `stage == "testing"` | `Testing` | +| `stage == "deploy"` | `⏸️ Awaiting Deploy — ожидание Confirm Deploy` | +| `stage == "done"` | `Done` | +| неизвестный/битый `stage` | дефолт: `html`-безопасная строка по `stage` (или `To Analyse`) | + +Этого слоя достаточно для **`⏸️ In Review`** и **`⏸️ Awaiting Deploy`** — оба +обязательны по DoD и **работают без сети** (AC-7, AC-8). `In Review` выводится +исключительно из brd-clock. + +**Слой 2 — best-effort live-overlay** `_live_plane_branch_override(repo, plane_issue_id, +base_label) -> str` для веток, неразличимых offline: **Needs Input, Blocked, Rejected, +Cancelled, Deploying, Monitoring after Deploy**. Алгоритм: + +1. Резолв `project_id` по `repo` (`get_project_by_repo(repo).plane_project_id`). +2. `live_uuid = fetch_issue_state(plane_issue_id, project_id)` (ORCH-060) — **с коротким + таймаутом** (см. Р-4), не дефолтным 10s. +3. Сопоставление `live_uuid` с **конкретными** UUID веток из + `get_project_states(project_id)` (кэш ORCH-010): `needs_input`, `blocked`, + `cancelled`, `rejected`, `deploying`, `monitoring`. +4. Override применяется **только** если `live_uuid` совпал с одним из этих ключей. + Иначе возвращается `base_label` (offline-метка). + +**Прецеденс (порядок приоритета):** +1. Если offline-ядро дало **`⏸️ In Review`** (brd-clock) — overlay **не вызывается**: + brd-clock авторитетнее возможно-устаревшего Plane-чтения для In Review. +2. Иначе `base_label` = offline-метка, затем применяется overlay (если включён и удался). + +**Анти-false-positive на enduro (важно):** на enduro-trails ключи `deploying`/ +`monitoring` алиасят UUID `in_progress`/`done` (`_STATE_ALIAS_FALLBACK`), поэтому прямое +сравнение UUID дало бы ложный `Deploying` для любой `in_progress`-задачи. Поэтому для +`deploying`/`monitoring` override применяется **только если** их UUID в +`get_project_states` **отличается** от UUID базового ключа (т.е. проект реально завёл +отдельный статус — это ORCH, не enduro). Ключи `needs_input/blocked/cancelled/rejected` +имеют отдельные UUID и на enduro, и на ORCH (`_DEFAULT_STATES`), поэтому различимы всегда. + +### Р-2. Fail-safe и невлияние на конвейер (overlay) + +- `_live_plane_branch_override` обёрнут в `try/except` и **никогда не бросает**; любая + ошибка/таймаут/нет сети/нет данных → возвращается `base_label`. Это удовлетворяет + «без сети не падать» и AC-9 (рендер карточки никогда не падает). +- Нет `plane_issue_id` / нет `project_id` / нет креды → overlay не вызывается, метка = + offline-ядро. +- **Kill-switch:** новый флаг конфигурации `tracker_live_status: bool = True` + (env `ORCH_TRACKER_LIVE_STATUS`). При `False` overlay полностью отключён (никаких + сетевых чтений в рендере) — карточка деградирует на offline-ядро. Это аварийный + тумблер и страховка от регресса для не-ORCH проектов. **Дефолт `True`**, иначе + обязательный по DoD `Needs Input` не отобразится из коробки. + +### Р-3. Кэш live-статуса (защита hot-path) + +`render_task_tracker` вызывается на КАЖДОМ обновлении трекера (старт/финиш агента, +переход стадии), а в режиме `bump` — с delete+send каждый раз. Чтобы серия быстрых +перерисовок не била по Plane: + +- Добавить **TTL-кэш per-issue** для `live_uuid` (ключ — `plane_issue_id`, TTL + `tracker_live_status_ttl_s: int = 60`). По образцу `_STATES_CACHE` в `plane_sync.py`. +- На промахе кэша — один `fetch_issue_state` с коротким таймаутом; результат кладётся в + кэш. На любой ошибке кэш не портится, возвращается offline-метка. + +Это ограничивает сетевую нагрузку overlay ~одним GET в `TTL` на задачу. + +### Р-4. Короткий таймаут live-чтения в рендере + +`fetch_issue_state` (ORCH-060) хардкодит `timeout=10`. Для пути рендера это слишком +долго (рендер синхронный, в линии переходов общего конвейера). Решение: добавить в +`fetch_issue_state` **необязательный параметр `timeout`** (дефолт прежний `10` — +обратная совместимость для reconciler), а overlay вызывает его с +`settings.tracker_live_status_timeout_s` (дефолт **3** с). Поведение/сигнатуры +существующих вызовов не меняются. + +### Р-5. Единый хелпер кликабельного номера `plane_issue_link` + +Добавить в `src/notifications.py`: + +```python +def plane_issue_link(work_item_id, plane_issue_id=None, project_id=None, repo=None) -> str: + """HTML с кликабельным номером (ORCH-NNN) или html.escape(work_item_id). + Никогда не падает.""" +``` + +- Переиспользовать логику и guard'ы `_build_plane_issue_link` (ORCH-017), **разнеся** + «текст ссылки = номер задачи» и «текст ссылки = `✅ Задача в Plane`», чтобы не + дублировать резолв проекта и loopback-guard. Рекомендуется выделить приватный + `_plane_issue_url(repo, plane_issue_id, project_id) -> str | None` (сборка URL + + loopback/workspace/project guard), который зовут оба: `plane_issue_link` (текст = + номер) и `_build_plane_issue_link` (текст = «✅ Задача в Plane»). +- База URL: `plane_web_url` → fallback `plane_api_url`; loopback → «нет web URL» + (`_is_loopback_base`). +- `project_id`: явный аргумент → иначе резолв по `repo`. +- URL: `{web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/`. +- Текст = `html.escape(work_item_id)`; `href` = `html.escape(url, quote=True)`. +- **Fail-safe:** не хватает любого из (web_base/не-loopback, workspace, project_id, + plane_issue_id) → вернуть `html.escape(work_item_id)` (номер без ссылки). Никогда не + бросает (AC-11, AC-12). + +### Р-6. Доступ к `plane_issue_id`/`project_id` в точках уведомлений (требование 4) + +В большинстве точек `send_telegram` доступен только `work_item_id`. Решение — +тонкая fail-safe обёртка по образцу `_get_task_link_fields`: + +```python +def link_for(work_item_id, task_id=None) -> str: + """По work_item_id (или task_id) достать repo+plane_issue_id из БД и вернуть + plane_issue_link(...). На любой нехватке данных -> html.escape(work_item_id).""" +``` + +- Если у точки есть `task_id` — читать `(repo, plane_issue_id)` напрямую из `tasks` по + `id`. Если только `work_item_id` — `SELECT repo, plane_issue_id FROM tasks WHERE + work_item_id=? ORDER BY id DESC LIMIT 1` (как в `_resolve_project_id`). +- Везде, где данных нет — деградация на `html.escape(work_item_id)`, без падения. +- Применить во всех точках из TZ §3.3 (`notify_approve_requested`, `notify_error`, + `stage_engine`, `launcher`, `merge_gate`, `job_reaper`, `security_gate`, `reconciler`, + `main`) — **только там, где упоминается номер задачи**. + +### Р-7. `tracker_mode` дефолт → `bump` + +`src/config.py`: `tracker_mode: str = "edit"` → `"bump"`. Инвариант «одна карточка на +задачу» сохранён в обоих режимах (код `update_task_tracker` не меняется по сути). +`edit` остаётся доступен через `ORCH_TRACKER_MODE=edit`. Транзиентный фейл `send` не +обнуляет `tracker_message_id` (инвариант уже в коде — сохранить). + +### Р-8. Чего НЕ делаем (границы) + +- НЕ менять схему БД, `STAGE_TRANSITIONS`, Quality Gates, транспорт + `send_telegram`/`edit_telegram`/`delete_telegram`, `disable_notification`-семантику. +- НЕ менять поведение агентов/конвейера. Слой B (индикация) не управляет слоем A. +- НЕ добавлять блокирующих сетевых ожиданий в линию переходов сверх одного короткого + best-effort GET с кэшем (Р-3/Р-4). +- НЕ создавать глобальный (сквозной) ADR: изменение локально для `notifications.py` + + один config-дефолт, не вводит новую стадию/QG/компонент. Достаточно per-work-item ADR. + +## Последствия + +**Плюсы** +- Обязательные по DoD `⏸️ In Review`, `⏸️ Awaiting Deploy` работают **без сети** + (детерминированно, тестируемо offline — AC-6/AC-7). +- `❓ Needs Input` (и Blocked/Rejected/Cancelled/Deploying/Monitoring) отражаются через + авторитетный источник — живой Plane-статус, который иначе невосстановим из БД. +- Единый хелпер ссылки убирает дублирование резолва проекта/loopback-guard (ORCH-017). +- Kill-switch + кэш + короткий таймаут ограничивают риск для общего инстанса. + +**Минусы / ограничения** +- Overlay добавляет ≤1 короткий GET (3 с таймаут) на задачу в `TTL=60s` в путь рендера. + Митигировано кэшем, таймаутом и kill-switch. +- При недоступном Plane ветки `Needs Input`/`Blocked`/… деградируют на offline-метку + (`Analysis`/stage). Это осознанный, безопасный компромисс (рендер важнее точности + ветки; конвейер не блокируется). +- На частично сконфигурированном проекте без отдельных статусов `Deploying`/`Monitoring` + эти ветки не показываются (alias-guard) — корректная деградация, не баг. + +**Риски** — см. `10-tech-risks.md`. + +## Альтернативы (отклонены) + +- **Только offline (вариант б TZ).** Отклонён: не отличает `Needs Input` от `Analysis` + → не покрывает обязательный AC-8. +- **Чтение `01-questions.md` из worktree как offline-сигнал Needs Input.** Отклонён: + хрупко (резолв пути worktree из `notifications.py`, файл может пережить ответ, + гонки) — менее надёжно, чем авторитетный Plane-статус. +- **Добавить DB-колонку-флаг для ветки.** Запрещено TZ (без изменения схемы). +- **Асинхронный фон/демон для подтяжки статуса.** Избыточно для слоя индикации; кэш + + короткий таймаут дешевле и проще, без нового компонента. diff --git a/docs/work-items/ORCH-067/07-infra-requirements.md b/docs/work-items/ORCH-067/07-infra-requirements.md new file mode 100644 index 0000000..f0dbf5b --- /dev/null +++ b/docs/work-items/ORCH-067/07-infra-requirements.md @@ -0,0 +1,46 @@ +# Инфраструктурные требования — ORCH-067 + +Топология не меняется (никаких новых контейнеров/портов/сервисов). Изменения — +**только конфигурация/env** и обязательный staging-гейт (self-hosting). + +## 1. Изменения конфигурации (`src/config.py`) + +| Поле | env | Старое | Новое | Назначение | +|---|---|---|---|---| +| `tracker_mode` | `ORCH_TRACKER_MODE` | `"edit"` | `"bump"` (дефолт) | Карточка падает вниз ленты при обновлении (ADR-001 Р-7). `edit` доступен через env. | +| `tracker_live_status` | `ORCH_TRACKER_LIVE_STATUS` | — (нет) | `True` (дефолт) | Kill-switch live-overlay Plane-статуса (ADR-001 Р-2). `0/false` → только offline-метки, без сетевых чтений в рендере. | +| `tracker_live_status_ttl_s` | `ORCH_TRACKER_LIVE_STATUS_TTL_S` | — | `60` | TTL per-issue кэша live-статуса (ADR-001 Р-3). | +| `tracker_live_status_timeout_s` | `ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S` | — | `3` | Короткий таймаут live-чтения в рендере (ADR-001 Р-4). | + +Уже существующие (не менять, использовать): `plane_web_url` +(`ORCH_PLANE_WEB_URL`, прод — `https://plane.mva154.duckdns.org`), +`plane_workspace_slug` (прод — `ag_proj`), `plane_api_url`. + +## 2. `.env` / `.env.example` + +- Обновить `.env.example`: добавить `ORCH_TRACKER_MODE`, `ORCH_PLANE_WEB_URL`, + `ORCH_TRACKER_LIVE_STATUS*` с дефолтами и комментариями (канон настроек — + `.env.example`, реальные секреты не коммитить). +- На прод-хосте допустимо явно выставить `ORCH_TRACKER_MODE=bump` как страховку, но код + обязан работать «из коробки» и без env. +- `ORCH_PLANE_WEB_URL` должен быть задан на проде (иначе номер задачи деградирует на + текст без ссылки — fail-safe, не падение). + +## 3. Self-hosting (обязательно) + +- **НЕ перезапускать / не ронять** прод-контейнер `orchestrator` (8500) в рамках задачи — + общий инстанс/БД с enduro-trails. +- Обязательная страховка через `deploy-staging` (8501, изолированная БД) **до** прод-деплоя. + На staging проверить: + - режим `bump`: одна карточка на задачу, падает вниз, тихо (без звука), без дублей; + - статус-строка: `⏸️ In Review`, `⏸️ Awaiting Deploy`, `❓ Needs Input` отображаются; + - кликабельный номер ведёт на страницу Plane; + - **нет регресса для enduro-trails** (карточка без новых статусов деградирует корректно). +- Прод-деплой орка — только переводом задачи на стадии `deploy` в статус + **«Confirm Deploy»** (ORCH-059), не `Approved`. + +## 4. Сетевые требования + +- Live-overlay требует доступности Plane API (`plane_api_url`) из контейнера — он уже + есть (используется plane_sync). Недоступность Plane → graceful degrade на offline-метку, + конвейер не блокируется (короткий таймаут + kill-switch). diff --git a/docs/work-items/ORCH-067/08-data-requirements.md b/docs/work-items/ORCH-067/08-data-requirements.md new file mode 100644 index 0000000..653f64b --- /dev/null +++ b/docs/work-items/ORCH-067/08-data-requirements.md @@ -0,0 +1,35 @@ +# Требования к данным — ORCH-067 + +## Изменения схемы БД: НЕТ + +`STAGE_TRANSITIONS`, таблицы и колонки `tasks`/`agent_runs` **не меняются**. Это жёсткое +ограничение TZ §6 и предпосылка ADR-001 (запрет колонки-флага для веток статуса). + +## Читаемые колонки `tasks` (существующие) + +| Колонка | Использование в ORCH-067 | +|---|---| +| `id` | Ключ задачи. | +| `work_item_id` | Текст номера (`ORCH-NNN`) + ключ резолва в `link_for`. | +| `title` | Заголовок карточки (`html.escape`). | +| `stage` | Offline-маппинг Plane-статуса (ADR-001 Р-1, слой 1). | +| `brd_review_started_at`, `brd_review_ended_at` | Различение `Analysis` ↔ `⏸️ In Review` (offline, без сети). | +| `repo` | Резолв `project_id` (`get_project_by_repo`) для ссылки и live-overlay. | +| `plane_issue_id` (UUID) | `issue_id` в URL Plane + аргумент `fetch_issue_state` (live-overlay). | +| `created_at`, `updated_at` | Тоталы времени в done-строке (без изменений). | + +`render_task_tracker` **расширяет существующий `SELECT`** по `tasks`, добавляя `repo` и +`plane_issue_id` к уже выбираемым полям. Схему это не трогает — колонки уже есть. + +## Кэш в памяти (не БД) + +Per-issue TTL-кэш live-статуса (ключ `plane_issue_id`, TTL +`tracker_live_status_ttl_s=60`, ADR-001 Р-3) — **in-memory**, по образцу `_STATES_CACHE` +в `plane_sync.py`. Не персистится, переживание рестарта не требуется (best-effort +индикация). Очистка при рестарте — допустима. + +## Источник имён статусов + +Имена и логические ключи статусов берутся из существующих структур `src/plane_sync.py` +(`_PLANE_NAME_TO_KEY`, `get_project_states`, `_DEFAULT_STATES`), вводимых ORCH-066. +Новых статусов/ключей ORCH-067 **не добавляет**. diff --git a/docs/work-items/ORCH-067/10-tech-risks.md b/docs/work-items/ORCH-067/10-tech-risks.md new file mode 100644 index 0000000..35fb981 --- /dev/null +++ b/docs/work-items/ORCH-067/10-tech-risks.md @@ -0,0 +1,21 @@ +# Технические риски — ORCH-067 + +| # | Риск | Вероятность / Влияние | Митигация (ADR-001) | Остаточный риск | +|---|---|---|---|---| +| R-1 | **Регресс enduro-trails** при смене дефолта `tracker_mode` → `bump` (другое поведение карточки для всех проектов). | Сред / Сред | Инвариант «одна карточка на задачу» сохранён; `edit` доступен через env; проверка на staging + тесты нерегресса (AC-16). | Низкий | +| R-2 | **Поломка HTML-разметки** неэкранированным `title`/причиной → сообщение с `parse_mode=HTML` не доставится. | Сред / Сред | Обязательный `html.escape` для всего пользовательского текста; `href` через `html.escape(url, quote=True)`; тест с ``/`&` (AC-14). | Низкий | +| R-3 | **Latency в hot-path конвейера**: live-overlay добавляет сетевой GET в синхронный рендер, вызываемый на каждом переходе/в bump. | Сред / Сред | Короткий таймаут 3 с (Р-4) + per-issue TTL-кэш 60 с (Р-3) + kill-switch `ORCH_TRACKER_LIVE_STATUS=0` (Р-2). ≤1 GET на задачу за TTL. | Низкий | +| R-4 | **Рендер карточки падает** на битых данных/недоступном Plane. | Низк / Выс | `plane_status_label` чистая и never-raise; overlay в `try/except` → degrade на offline-метку; `render_task_tracker` уже never-raise (AC-9). | Очень низкий | +| R-5 | **Ложный `Deploying`/`Monitoring` на enduro** (их UUID алиасит `in_progress`/`done`). | Сред / Низк | Override этих веток только если UUID статуса ≠ UUID базового ключа в `get_project_states` (Р-1, anti-false-positive). | Очень низкий | +| R-6 | **Устаревший Plane-статус из кэша** показывает неактуальную ветку (например, `Needs Input` после ответа). | Сред / Низк | TTL 60 с самозаживает; offline-ядро авторитетно для In Review (brd-clock не оверрайдится). Индикация, не управление — расхождение косметическое. | Низкий | +| R-7 | **Транзиентный фейл `send` плодит дубли / обнуляет указатель** в bump. | Низк / Сред | Инвариант уже в коде (`set_tracker_message_id` только при `new_mid is not None`); не менять; тест AC-3. | Низкий | +| R-8 | **Self-hosting**: деплой орка ломает общий инстанс (enduro + ORCH, общая БД/очередь). | Низк / Выс | Обязательный staging-гейт (8501) до прода; прод-контейнер не ронять в задаче; прод-деплой только через «Confirm Deploy». | Низкий | +| R-9 | **Пропущенная точка** уведомления с сырым номером (требование 4 — много call-sites). | Сред / Низк | Единый `link_for`/`plane_issue_link`; чек-лист точек из TZ §3.3; reviewer проверяет покрытие (AC-13). | Низкий | +| R-10 | **Рассинхрон имён статусов** с ORCH-066, если та не в проде на момент разработки. | Низк / Низк | Имена берутся из `_PLANE_NAME_TO_KEY` (golden source); делать после прода ORCH-066 (BRD §6). | Низкий | + +## Сводно + +Все остаточные риски — низкие/очень низкие после митигаций. Главные защитные контуры: +(1) offline-ядро статуса не требует сети и детерминировано; (2) live-overlay полностью +best-effort с таймаутом+кэшем+kill-switch; (3) обязательный staging-гейт перед прод-деплоем +общего инстанса (self-hosting).