---
work_item: ORCH-093
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
---

# 03 — Критерии приёмки (Acceptance Criteria): ORCH-093 — ретрай транзиентных merge-ошибок Gitea + гард already-in-main

Work Item: **ORCH-093** · Repo: **orchestrator** · Стадия: analysis

Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что
считается провалом). Reviewer/tester проверяет их буквально по файлам репозитория и тестам.

---

## AC-1 — ретрай транзиента 405/5xx/таймаут → успешный мерж

**Условие:** `merge_pr` при транзиентной ошибке мержа повторяет `POST …/merge` с backoff и
доводит мерж до успеха в пределах бюджета.
- **PASS:** мок httpx даёт на `POST …/merge` `405` дважды, затем `200` → `merge_pr` возвращает
  `(True, …)`, выполнено ровно 3 `POST`, ложного `False` нет. Аналогично для `5xx` и
  таймаута/сетевой ошибки в первых попытках.
- **FAIL:** `merge_pr` возвращает `False` на первом `405`/`5xx`/таймауте (one-shot), не делая
  повторных `POST`.

---

## AC-2 — реальный конфликт / не-mergeable НЕ ретраится (быстрый честный HOLD)

**Условие:** `merge_pr` при реальном конфликте (`409`/`422` с `mergeable==False`) или `403` не
зацикливается, а возвращает `(False, …)` быстро.
- **PASS:** мок httpx даёт `409` на `POST …/merge` и `GET /pulls/{n}` с `mergeable=False` →
  `merge_pr` возвращает `(False, …)` без дополнительных `POST` (не более одной попытки мержа);
  reason различим как терминальный. `403` → немедленный `(False, …)`.
- **FAIL:** `merge_pr` ретраит реальный конфликт до исчерпания бюджета (вечный/долгий цикл),
  задерживая честный HOLD.

---

## AC-3 — исчерпание ретраев → (False, …) + защита ORCH-071/081 как прежде

**Условие:** если транзиент не проходит за `N` попыток, `merge_pr` возвращает `(False, …)` с
понятным reason; защита «deploy succeeded but not merged» срабатывает как раньше.
- **PASS:** мок даёт `405` на всех `N` попытках → `merge_pr` возвращает
  `(False, "merge failed after <N> attempts: HTTP 405")` (или эквивалент); в `_handle_merge_verify`
  неподтверждённый SHA-in-main → HOLD + алерт (поведение ORCH-071/081 неизменно). Тест на
  не-merged HOLD остаётся зелёным.
- **FAIL:** при исчерпании ретраев reason неинформативен; или защита HOLD не срабатывает / задача
  ошибочно уходит в `done`.

---

## AC-4 — гард «ветка уже в main» → нет мусорного PR, задача доходит до done

**Условие:** если ветка уже полностью в `main` (нет коммитов `origin/main..branch`),
`ensure_open_pr` не создаёт PR и возвращает `already-in-main`; финализатор доводит до `done`.
- **PASS:** мок: открытого code-PR нет, `git rev-list --count origin/main..branch == 0` →
  `ensure_open_pr` возвращает `("already-in-main", …)` и **не делает** `POST …/pulls`; в
  `_handle_merge_verify` этот статус пропускает `merge_pr` и `verify_merged_to_main` (SHA-in-main)
  подтверждает мерж → задача доходит до `done` без создания пустого PR.
- **FAIL:** `ensure_open_pr` создаёт новый пустой PR на уже влитой ветке, либо статус
  `already-in-main` ошибочно трактуется как `failed` (ложный HOLD).

---

## AC-5 — kill-switch / конфиг ретраев; дефолты задокументированы

**Условие:** ретрай-поведение конфигурируемо (число попыток, backoff, kill-switch); при выключении —
one-shot как сейчас; дефолты в `.env.example`.
- **PASS:** в `src/config.py` есть поля `merge_retry_enabled` / `merge_retry_max_attempts` /
  `merge_retry_backoff_base_s` / `merge_retry_backoff_max_s` с разумными дефолтами (≈3 / 2 / 5);
  `.env.example` содержит дескрипторы `ORCH_MERGE_RETRY_*`; при `merge_retry_enabled=False` тест
  подтверждает ровно одну попытку `POST` (one-shot).
- **FAIL:** ретрай захардкожен (нет флагов/kill-switch), или `.env.example` не обновлён, или при
  выключенном флаге поведение отличается от текущего one-shot.

---

## AC-6 — never-raise сохранён; регресс зелёный; доки обновлены

**Условие:** контракт never-raise `merge_pr`/`ensure_open_pr` цел; полный регресс зелёный;
документация и `CHANGELOG` обновлены.
- **PASS:** при любой HTTP/parse/сетевой ошибке (в т.ч. внутри ретрай-цикла и git-проверки гарда)
  функции возвращают безопасный кортеж, исключение не пробрасывается; `pytest tests/ -q` зелёный;
  merge-gate-раздел доки (`docs/architecture/README.md` / `CLAUDE.md`) и `CHANGELOG.md` описывают
  ретрай и гард already-in-main.
- **FAIL:** исключение пробрасывается в `advance_stage`; падает любой тест в `tests/`; доки/CHANGELOG
  не отражают изменение.

---

## AC-7 — инварианты self-hosting / INV-4 не нарушены

**Условие:** изменение не вводит прямых `push`/`force-push` в `main` и не трогает
`STAGE_TRANSITIONS`/`QG_CHECKS`/схему БД.
- **PASS:** мерж по-прежнему идёт только через Gitea PR-merge API; `git diff` не содержит правок
  `STAGE_TRANSITIONS` / состава `QG_CHECKS` / схемы БД; никаких новых вызовов `git push … main`.
- **FAIL:** появился прямой push в `main`, либо изменены `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД.

---

## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / FR-1, FR-2 |
| AC-2 | BR-2, BR-3 / FR-2 |
| AC-3 | BR-4 / FR-1 |
| AC-4 | BR-5 / FR-3, FR-4 |
| AC-5 | BR-6, BR-7 / FR-5 |
| AC-6 | NFR-1, NFR-6 / FR-1…FR-5 |
| AC-7 | NFR-2 / §6, §7 ТЗ |
