diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 7bb26b9..995d13c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -206,6 +206,39 @@ merge-в-main вообще**. Detached host-деплой лишь retag'ал о `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`, `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`. +#### Гарантированный код-PR перед merge-verify (ORCH-082 — фикс ложного HOLD «no open PR») +Под-гейт merge-verify (ORCH-071/073) детерминированно мержит **открытый** код-PR ветки в `main` +(`merge_pr`, фильтр `head.ref==branch` И `base.ref=="main"`). Но конвейер **не гарантировал**, что +к моменту merge у ветки этот PR есть: PR создаётся единственной `launcher._ensure_pr` **только** на +developer-пути и **только** при свежем worktree-коммите. На деплое ORCH-074 (08.06, первая задача +после ручных восстановлений `main`) у ветки не оказалось открытого код-PR → `merge_pr` вернул +`("False", "no open PR")` → защита ORCH-073 верно удержала задачу (HOLD, не ложный `done`), но это +лечило следствие. ORCH-082 закрывает **отсутствующий инвариант** «к merge-verify у ветки есть +открытый код-PR» аддитивно, внутри того же под-гейта, не трогая машину стадий: +- **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise): + `GET …/pulls?state=open` с фильтром `head.ref==branch` И `base.ref=="main"` (**идентичен** + `merge_pr`/ORCH-073 FR-3 — авто-docs-PR `base != main` НЕ код-PR) → `("existed", N)`; иначе + `POST …/pulls` → `("created", N)`; гонка «PR exists»/409/422 → повторный GET → `existed` (без + дублей); любая иная ошибка → `("failed", reason)`. +- **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и **ПЕРЕД** `merge_pr`: + `created|existed` → штатно к `merge_pr` → `verify_merged_to_main`; `failed` → честный HOLD+alert + через новый helper `_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от + not-merged HOLD; `result.note="pr-create-failed-hold"`), задача остаётся на `deploy`, БЕЗ отката + на development. +- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО + `verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` устраняет лишь + **ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде). +- **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания PR), + сохранив прежний триггер «только developer-путь». +- **Условность как ORCH-35/43/58/71:** kill-switch `merge_verify_autocreate_pr_enabled` (дефолт + `true`); область — `merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); non-self — + no-op. `False` → поведение ORCH-074 1:1. Идемпотентность из Gitea (наличие открытого PR), **без + миграции БД** (restart-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`, + exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push. + +Подробнее: [adr-0016](adr/adr-0016-ensure-open-pr-before-merge-verify.md) (amends 0013/0014); +детально — `docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`. + ### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано) Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 — diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index e496903..58bf7c0 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -21,12 +21,14 @@ Per-work-item решения живут в `docs/work-items//06-adr/ADR-NNN- | adr-0013 | Merge-в-main + пост-деплой верификация как условие `done` | accepted | 2026-06-08 | ORCH-071 | | adr-0014 | SHA-в-main — единственный критерий merge-verify + регресс-гард | accepted | 2026-06-08 | ORCH-073 | | adr-0015 | Зависимости задач (B ждёт A) + сериализация merge внутри репо | accepted | 2026-06-08 | ORCH-026 | +| adr-0016 | ensure_open_pr — гарантированный код-PR перед merge-verify | accepted | 2026-06-09 | ORCH-082 | > ⚠️ Историческая коллизия: номер `0007` занят двумя файлами — > `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md` > (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий -> свободный номер (текущий максимум — `0015`). +> свободный номер (текущий максимум — `0016`). > adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»). +> adr-0016 **amends** adr-0013/0014 (гарантирует открытый код-PR перед merge_pr, ORCH-082). ## Формат **Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded. diff --git a/docs/architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md b/docs/architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md new file mode 100644 index 0000000..ede581a --- /dev/null +++ b/docs/architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md @@ -0,0 +1,52 @@ +# ADR-0016: ensure_open_pr — гарантированный код-PR перед merge-verify (ORCH-082) + +## Статус +Accepted — амендмент к [adr-0013](adr-0013-merge-verify-gate.md) и +[adr-0014](adr-0014-merge-verify-sha-source-of-truth.md). Детально: +`docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`. + +## Контекст +Merge-verify (ORCH-071/073) — под-гейт ребра `deploy → done`: детерминированно мержит код-PR в +`main` (`merge_pr`) и подтверждает merge **только** по «SHA-в-main» (`verify_merged_to_main`, +ORCH-073). На деплое ORCH-074 (08.06) `merge_pr` вернул `("False", "no open PR")`: у ветки **не +было** открытого PR с `head==branch` И `base=="main"`. Защита ORCH-073 верно удержала задачу +(HOLD, не ложный `done`), но это лечило **следствие**. + +Первопричина (код-аудит): PR создаётся в конвейере **единственной** функцией +`launcher._ensure_pr`, вызываемой **только** на developer-пути и **только** при свежем +worktree-коммите. Любой сценарий без свежего developer-коммита (бойнс без правок, повторный +прогон, **ручное восстановление ветки/`main`** — случай ORCH-074) оставляет ветку без код-PR. +Инвариант «к merge-verify у ветки есть открытый код-PR» в конвейере **отсутствовал** → блокер +автономного деплоя (ORCH-54). + +## Решение +Аддитивно обеспечить инвариант **внутри того же под-гейта**, ПЕРЕД `merge_pr`, не трогая машину +стадий: + +1. **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise): + `GET …/pulls?state=open` с фильтром **`head.ref==branch` И `base.ref=="main"`** (идентичен + `merge_pr`/ORCH-073 FR-3 — авто-docs-PR не считается код-PR) → `("existed", N)`; иначе + `POST …/pulls` → `("created", N)`; гонка «PR exists» → повторный GET → `existed` (без дублей); + любая ошибка → `("failed", reason)`. +2. **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и ПЕРЕД `merge_pr`: + `created|existed` → штатно к `merge_pr`; `failed` → честный HOLD+alert через новый helper + `_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от not-merged HOLD), задача + остаётся на `deploy`, БЕЗ отката на development. +3. **Kill-switch `merge_verify_autocreate_pr_enabled`** (дефолт `True`); область — + `merge_verify_applies` (self-hosting / `merge_verify_repos`). `False` → поведение ORCH-074 1:1. +4. **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания + PR), сохранив прежний триггер «только developer-путь». + +## Последствия +- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО + `verify_merged_to_main` (SHA-в-main) + `check_main_regression`. Создание PR устраняет лишь + **ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде). +- **Без миграций:** идемпотентность выводится из Gitea (наличие открытого PR), схема БД не меняется + — restart-safe; повторный заход (reaper/reconciler/re-approve) → `existed`, дублей нет. +- **Инварианты целы:** `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`, + exit-коды хука, merge-gate (ORCH-043), image-freshness (ORCH-058) — без изменений; `main` не + push/force-push; never-raise на всём пути. +- **Наблюдаемость:** один однозначный исход в логах на проход — created / existed / failed; HOLD по + failed текстуально отличим от HOLD not-merged. +- **Минус:** код-PR может создаваться после прохождения гейтов — безопасно, т.к. гейты валидируют + код ветки, а merge-verify идёт ПОСЛЕ всех гейтов; PR — лишь механизм слияния, ревью не обходится. diff --git a/docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md b/docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md new file mode 100644 index 0000000..f10cbb7 --- /dev/null +++ b/docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md @@ -0,0 +1,221 @@ +# ADR-001: Гарантированный идемпотентный код-PR перед merge-verify (ensure_open_pr) + +- Work Item: **ORCH-082** (Plane-заголовок «ORCH-81») +- Repo: `orchestrator` (self-hosting) +- Связь: амендмент к merge-verify ([adr-0013](../../../architecture/adr/adr-0013-merge-verify-gate.md), + [adr-0014](../../../architecture/adr/adr-0014-merge-verify-sha-source-of-truth.md)); + глобально зафиксировано в [adr-0016](../../../architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md) +- BRD/ТЗ/AC: `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md` + +## Статус +Accepted + +## Контекст + +### Что случилось (инцидент ORCH-074, 08.06) +Деплой ORCH-074 встал на под-гейте merge-verify (ребро `deploy → done`): +`run_deploy_finalizer → _handle_merge_verify` вызвал `merge_gate.merge_pr(repo, branch)` и +получил `ok=False, "no open PR"` — в Gitea для ветки `feature/ORCH-074-…` **не было открытого +PR** с `head.ref==branch` И `base.ref=="main"`. Защита ORCH-073 (fail-closed по «SHA-в-main») +**отработала правильно**: задача удержана на `deploy` (НЕ `done`), Plane → Blocked, Telegram-alert, +ложно-зелёного `done` не произошло. Разблокировано вручную — PR #79 создан через Gitea API, +finalizer перезапущен, код честно влит, задача `done`. Это **workaround, не фикс**. + +### Root cause (G1, подтверждён код-аудитом) +PR создаётся в конвейере **ровно в одном месте** — `AgentLauncher._ensure_pr` +(`src/agents/launcher.py:1079`), и вызывается он из `_monitor_agent` **только** по цепочке +условий (`src/agents/launcher.py:751–753`): + +``` +exit_code == 0 + → git status --porcelain непусто (есть worktree-изменения) + → git commit succeeded + → git push succeeded + → agent == "developer" ←── ТОЛЬКО здесь вызывается self._ensure_pr(...) +``` + +Отсюда класс «ветка без PR» структурно неизбежен. Подтверждённые код-аудитом ветви: + +- **R-A (условное создание) — структурный первопричинный дефект.** Если в конкретном + developer-run нет свежих изменений (`git status` пуст: ветка уже была закоммичена/запушена + ранее, бойнс REQUEST_CHANGES без новых правок, повторный прогон, **ручное восстановление + ветки**) — `_ensure_pr` **не вызывается вовсе**. PR не появится никогда. Никакого + персистентного флага «PR создан» в БД нет, поэтому идемпотентность чтения внутри `_ensure_pr` + до merge-стадии не доходит. +- **R-C (разъехавшееся состояние ветки/PR) — проксимальный триггер ORCH-074.** ORCH-074 — первая + задача после серии **ручных восстановлений `main` 08.06**: открытый код-PR был закрыт/не + пересоздан, у ветки мог остаться лишь авто-docs-PR (`base != main`), который `merge_pr` (фильтр + `base=="main"`, ORCH-073 FR-3) корректно НЕ считает кодовым. +- **R-B (тихий сбой создания) — потенциальная, не первопричина здесь.** `_ensure_pr` глотает любое + исключение (`except Exception → logger.error → return None`): транзиентная ошибка Gitea на + `POST …/pulls` теряется без ретрая и эскалации. + +**Вывод:** в конвейере **отсутствует инвариант** «к моменту merge-verify у ветки есть открытый +код-PR». Защита ORCH-073 верно ловит следствие, но причина — выше по потоку. Любая следующая +задача с тем же стечением обстоятельств застрянет тем же образом → автономный деплой (ORCH-54) +заблокирован. + +### Ограничения, которые нельзя нарушать +- Защита ORCH-073 (SHA-в-main + регресс-гард) — приоритетна. Создание PR **не должно** маскировать + реально невлитый код. +- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схема БД, `check_deploy_status`/`_parse_deploy_status`, + exit-коды хука, merge-gate (ORCH-043), image-freshness (ORCH-058) — без изменений. +- Весь путь merge-verify — **never-raise**. +- Слияние только через PR; `main` никогда не push/force-push. + +## Решение + +Закрыть пробел инвариантом «обеспечить открытый код-PR» **внутри того же под-гейта merge-verify**, +ПЕРЕД детерминированным `merge_pr`. Три точечные врезки, симметричные существующему дизайну +ORCH-071/073 (leaf-актор в `merge_gate` + врезка в `_handle_merge_verify` + kill-switch). Машина +стадий и реестры не трогаются. + +### Р-1. Новый идемпотентный leaf-актор `merge_gate.ensure_open_pr(repo, branch)` + +Сигнатура (решение архитектора по ТЗ §1): + +```python +def ensure_open_pr(repo: str, branch: str) -> tuple[str, str]: + """Гарантировать открытый код-PR (head==branch, base==main). never-raise. + Возврат: ("existed", "") | ("created", "") | ("failed", ""). + """ +``` + +Алгоритм (FR-1): +1. `GET …/pulls?state=open` → найти PR с **`head.ref==branch` И `base.ref=="main"`**. Фильтр + **идентичен** `merge_pr`/ORCH-073 FR-3 — авто-docs-PR (`base != main`) НЕ считается код-PR + (AC-6). Нашли → `("existed", )`. +2. Иначе `POST …/pulls` (`head=branch`, `base="main"`, авто-заголовок/тело) → `201` → + `("created", )`. +3. **Идемпотентность при гонке:** если на `POST` Gitea вернёт «PR exists»/`409`/`422` — + повторный `GET` (шаг 1) подтверждает существующий PR → `("existed", …)`. Дубль не плодится + (AC-2, FR-5). +4. Любая иная HTTP/parse/сетевая ошибка → `("failed", )`. **Never-raise** (`except + Exception → ("failed", str(e))`). + +Актор — **leaf** (зависит только от `settings` + `httpx`, без импорта `stage_engine`), как +`merge_pr`/`verify_merged_to_main`. Таймауты — переиспользовать `settings.merge_pr_timeout_s` +(тот же класс Gitea-вызовов). + +> **Почему фильтр `base=="main"` критичен** (грабли ORCH-073): у ветки одновременно бывают код-PR +> и авто-docs-PR. Без фильтра актор «увидит» docs-PR как existed и не создаст нужный код-PR, а +> `merge_pr` потом не найдёт что мержить → петля. Один и тот же предикат `head==branch && +> base=="main"` гарантирует, что `ensure_open_pr` и `merge_pr` работают с одним и тем же PR. + +### Р-2. Врезка в `_handle_merge_verify` (ребро `deploy → done`) + +В существующем `_handle_merge_verify` (`src/stage_engine.py:1324`), **ПОСЛЕ** +`merge_verify_applies(repo)`-гейта и резолва `sha = image_freshness.validated_revision(...)`, +но **ПЕРЕД** `merge_pr`: + +```python +sha = image_freshness.validated_revision(repo, branch) + +# ORCH-082: гарантировать открытый код-PR ДО детерминированного merge_pr. +if settings.merge_verify_autocreate_pr_enabled: + pr_status, pr_detail = merge_gate.ensure_open_pr(repo, branch) + logger.info( + f"Task {task_id}: merge-verify ensure_open_pr -> {pr_status} ({pr_detail})" + ) + if pr_status == "failed": + return _hold_pr_create_failed( + task_id, repo, work_item_id, branch, pr_detail, result + ) + # "created" | "existed" -> штатно продолжаем к merge_pr. + +merged_ok, merge_msg = merge_gate.merge_pr(repo, branch) +... +``` + +Семантика (FR-2): +- `created | existed` → продолжаем штатно к `merge_pr` → `verify_merged_to_main` → регресс-гард. +- `failed` → **честный HOLD + alert** через новый helper `_hold_pr_create_failed` (см. Р-3); задача + остаётся на `deploy` (НЕ `done`), БЕЗ отката на development — симметрично текущему not-merged/ + regressed HOLD. +- kill-switch off → блок пропускается целиком → поведение 1:1 как до фикса (AC-8). + +Место выбрано так, что **никакой существующий шаг не сдвигается**: `merge_pr` и +`verify_merged_to_main` остаются на своих местах с теми же контрактами. Создание PR — это только +страховка инварианта ДО них. + +### Р-3. Новый HOLD-helper `_hold_pr_create_failed` (распознаваемость причины, FR-4/AC-5) + +Зеркало существующего `_hold_main_regressed` (`src/stage_engine.py:1280`). Текст HOLD **обязан +отличаться** от not-merged HOLD: оператор должен видеть, что причина — **невозможность создать +PR** (Gitea недоступна), а не **невозможность слить уже созданный**: + +```python +def _hold_pr_create_failed(task_id, repo, work_item_id, branch, reason, result) -> bool: + merge_gate.note_not_merged_alert(work_item_id) # переиспользуем счётчик-нотификатор + msg = (f"PR создать не удалось: {reason} (repo={repo}, branch={branch}, " + f"wi={work_item_id}). Открытый код-PR отсутствует и не создан — задача " + f"удержана на `deploy` (НЕ done). Нужно проверить доступность Gitea / создать PR.") + # set_issue_blocked + plane_add_comment + send_telegram (каждый в try/except, never-break HOLD) + result.alerted = True + result.note = "pr-create-failed-hold" # отличается от "merge-not-verified-hold" + result.advanced = False + return True +``` + +Это сохраняет инвариант «никогда не пробрасываем исключение в `advance_stage`»: `failed` — +структурированный исход, а не throw. + +### Р-4. Единый источник кода создания PR (опционально, рекомендуется) + +`launcher._ensure_pr` рекомендуется **делегировать** в `merge_gate.ensure_open_pr`, чтобы создание +PR жило в одном месте и одинаково логировало created/existed/failed (G3). **Поведенческий +инвариант:** триггер «создавать PR только в developer-пути со свежим коммитом» **НЕ ужесточается** +(BRD/ТЗ §1) — меняется лишь реализация под капотом, не условие вызова. Это снижает риск +рассинхрона двух копий логики «выбрать/создать PR». Если делегирование увеличивает диффу/риск — +допустимо оставить `_ensure_pr` как есть и лишь усилить его логирование (created/existed/failed); +функциональная цель ORCH-082 достигается врезкой Р-2 независимо. + +### Р-5. Kill-switch и область действия + +- `merge_verify_autocreate_pr_enabled: bool = True` + (env `ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED`) в `src/config.py`, рядом с + `merge_verify_enabled`/`regression_guard_enabled`. +- `False` → ровно прежнее поведение: авто-создания нет, «no open PR» → HOLD как в ORCH-074 (AC-8). +- Область — `merge_gate.merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); прочие + репо — no-op, создание PR остаётся за прежним механизмом (AC-9). Отдельного `*_repos` для + авто-создания НЕ вводим: семантически оно неотделимо от merge-verify, у которого уже есть область. + +## Последствия + +### Плюсы +- Закрыт структурный пробел: к merge-verify ветка гарантированно имеет открытый код-PR; ложный + HOLD «no open PR» больше не требует ручного вмешательства (AC-2/AC-3). +- Защита ORCH-073 цела и приоритетна: верификация остаётся **только** `verify_merged_to_main` + (SHA-в-main) + `check_main_regression`. Реально невлитый код → HOLD как прежде (AC-4/FR-3). +- Идемпотентность по факту Gitea (наличие открытого PR), без новой колонки/таблицы — согласуется с + restart-safe-моделью merge-verify; повторный заход (reaper/reconciler/re-approve) → `existed`, + дублей нет (FR-5/AC-2). +- Распознаваемые исходы в логах и в HOLD-тексте: created / existed / failed (G3/AC-5). +- Инварианты сохранены: `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`, + exit-коды хука, merge-gate, image-freshness — не тронуты (AC-10). `main` не push/force-push. + +### Минусы / ограничения +- Auto-создание PR на ребре `deploy → done` означает, что код-PR может появиться **после** того, + как все гейты (security/merge-gate/staging/image-freshness) уже пройдены по ветке. Это безопасно + по времени (BRD §6 допущение): ревью/гейты валидируют **код ветки**, а PR — лишь механизм + слияния; merge-verify исполняется ПОСЛЕ всех гейтов. PR здесь не обходит ревью. +- При недоступности Gitea задача попадёт в HOLD (как и сегодня) — но теперь с явным текстом + «PR создать не удалось» вместо «PR не влит». Это сознательный fail-closed (AC-7): never-raise, + честный HOLD, не ложно-зелёный `done`. +- Небольшое дублирование Gitea-вызовов между `ensure_open_pr` и `merge_pr` (оба GET список PR). Это + приемлемо: два независимых leaf-актора с одинаковым фильтром важнее микро-оптимизации; объединять + в один вызов — увеличить связность без пользы. + +### Влияние на self-hosting +Изменение строго аддитивно и под kill-switch (`True`). Прод-контейнер не рестартится этой задачей; +выкат — через staging-гейт (8501) как любая ORCH-задача. На ребре `deploy → done` риск-профиль не +растёт: при любом сбое — HOLD, не падение `advance_stage`, конвейер всех проектов не встаёт. + +## Связанные документы +- BRD/ТЗ/AC: `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md` +- Тех-риски: `10-tech-risks.md` +- Глобальный амендмент: [adr-0016](../../../architecture/adr/adr-0016-ensure-open-pr-before-merge-verify.md) +- Контекст merge-verify: [adr-0013](../../../architecture/adr/adr-0013-merge-verify-gate.md), + [adr-0014](../../../architecture/adr/adr-0014-merge-verify-sha-source-of-truth.md) +- Постмортем фантомного merge: `docs/history/LESSONS_2026-06-08_phantom-merge.md`, + runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` diff --git a/docs/work-items/ORCH-082/10-tech-risks.md b/docs/work-items/ORCH-082/10-tech-risks.md new file mode 100644 index 0000000..9829afc --- /dev/null +++ b/docs/work-items/ORCH-082/10-tech-risks.md @@ -0,0 +1,27 @@ +# 10 — Технические риски: ORCH-082 (ORCH-81) + +Риски точечной врезки «ensure_open_pr перед merge-verify». Все — в зоне ребра `deploy → done` +(self-hosting), под kill-switch `merge_verify_autocreate_pr_enabled`. + +| ID | Риск | Вероятн. | Влияние | Митигация | +|----|------|----------|---------|-----------| +| **R1** | `ensure_open_pr` выбирает/создаёт **не тот** PR (авто-docs-PR `base != main`) → `merge_pr` мержит/верифицирует не тот PR | Сред. | Высокое | Фильтр `head.ref==branch` И `base.ref=="main"`, **идентичный** `merge_pr` (ORCH-073 FR-3). Тест AC-6: ветка с docs-PR (`base!=main`) → актор его игнорирует и создаёт код-PR на `main`. | +| **R2** | Создание PR **маскирует** реально невлитый код → ложно-зелёный `done` (регресс ORCH-073) | Низк. | Критич. | Верификация остаётся ТОЛЬКО `verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` НЕ влияет на вердикт merge. Регресс-тест AC-4: `verify_merged_to_main→False` ⇒ HOLD, не `done`. | +| **R3** | Гонка: параллельно создаётся 2 PR → дубль | Низк. | Сред. | Идемпотентность FR-1.3: на ошибку «PR exists»/409/422 — повторный GET → `existed`; PR создаётся только если GET пуст. Тест AC-2. | +| **R4** | Исключение из `ensure_open_pr` пробрасывается в `advance_stage` → падение перехода | Низк. | Высокое | Контракт never-raise (`except Exception → ("failed", reason)`); врезка обёрнута внешним try/except `_handle_merge_verify`. `failed` → структурированный HOLD, не throw. Тест AC-7. | +| **R5** | Gitea недоступна на ребре `deploy → done` → задача в HOLD | Низк. | Сред. | Сознательный fail-closed: `failed` → честный HOLD+alert (`_hold_pr_create_failed`), НЕ ложный `done`. Текст HOLD отличим от not-merged (AC-5) — оператор видит причину. Reaper/reconciler/re-approve переиграют, когда Gitea вернётся (FR-5). | +| **R6** | Оператор не различит HOLD «PR не создан» и HOLD «PR не влит» | Сред. | Низк. | Отдельный helper `_hold_pr_create_failed` с собственным текстом и `result.note="pr-create-failed-hold"` (≠ `merge-not-verified-hold`); лог-строка `ensure_open_pr -> failed: `. AC-5. | +| **R7** | Расхождение логики выбора/создания PR между `launcher._ensure_pr` и `merge_gate.ensure_open_pr` | Сред. | Сред. | Рекомендованное делегирование `_ensure_pr → ensure_open_pr` (единый код). Если не делегируем — обе копии используют ОДИН фильтр `head==branch && base==main`; тест на согласованность. | +| **R8** | Включение по умолчанию (`True`) меняет прод-поведение скрытно | Низк. | Сред. | Поведение строго аддитивно: при наличии PR → `existed`/no-op; меняется лишь ранее-падавший путь «no open PR». Kill-switch `False` → 1:1 ORCH-074 (AC-8). Выкат через staging-гейт (8501). | +| **R9** | Регресс инвариантов (`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/exit-коды) | Низк. | Высокое | Под-гейт-врезка в `advance_stage`, НЕ новый `QG_CHECKS`-элемент и НЕ новая стадия; БД не трогается (идемпотентность из Gitea). Тест AC-10 + полный `pytest`. | + +## Зоны без изменений (подтверждение границ) +- **Инфраструктура/топология** — без изменений → `07-infra-requirements.md` не требуется. +- **Схема БД** — без изменений (идемпотентность выводится из Gitea) → `08-data-requirements.md` + не требуется. +- `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука, + merge-gate (ORCH-043), image-freshness (ORCH-058), terminal-sync — не тронуты. + +## Главный архитектурный приоритет +При любом конфликте «создать PR» **проигрывает** «не дать ложно-зелёный `done`» (защита ORCH-073). +Создание PR — страховка инварианта ДО merge_pr, никогда не подмена верификации merge.