# adr-0014: SHA-в-main — единственный критерий merge-verify + регресс-гард целостности `main`

- **Статус:** accepted
- **Дата:** 2026-06-08
- **Задача:** ORCH-073 (BUG CRITICAL — эрозия `main`)
- **Amends:** [adr-0013](adr-0013-merge-verify-gate.md) (ORCH-071) — меняет КРИТЕРИЙ подтверждения merge.
- **Детальный ADR:** `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`

## Контекст

adr-0013 (ORCH-071) ввёл под-гейт merge-verify на ребре `deploy → done`, но допускал
подтверждение merge по **ИЛИ-критерию**: `verify_merged_to_main` возвращал `True`, если
`pr_already_merged(repo, branch)` **ЛИБО** SHA — предок `origin/main`. `pr_already_merged`
засчитывал **любой** merged PR ветки, включая авто docs-PR (staging/deploy-логи). У одной
feature-ветки в `main` сливались только docs-PR, а code-PR — нет → `pr_already_merged`=`True` →
verify `CONFIRMED` → `done`, хотя кода в `main` не было. Накопительно потеряны ORCH-067 (ссылки
`plane_issue_link`) и ORCH-069 (`qg0_title_max`). Вторичный усилитель — CHANGELOG-ребейзы,
откатывающие ветку и тащащие устаревший код-сосед. Восстановление кода (G1) выполнено вручную
restore-PR #76; этот ADR устраняет корень навсегда.

## Решение

1. **SHA-в-main — единственный критерий (FR-1).** `verify_merged_to_main(repo, branch, sha)`
   подтверждает merge **ТОЛЬКО** прямым фактом `git merge-base --is-ancestor <sha> origin/main`
   (после `git fetch origin main`). OR-ветка `pr_already_merged` **удалена** из верификатора.
   Пустой `sha` / любая git-ошибка → `False` (fail-closed: alert + HOLD). never-raise (INV-1).
2. **`pr_already_merged` → idempotency-guard, различающий code-PR/docs-PR (FR-2).** Засчитывает
   merged PR только при `head.ref==<feature-branch>` И `base.ref=="main"` (явный фильтр в цикле,
   не ненадёжный query-параметр `head`). Используется лишь как защита `merge_pr` от второго merge,
   НЕ как подтверждение `done`.
3. **`merge_pr` сливает именно code-ветку (FR-3).** Выбор открытого PR по `head.ref==branch` И
   `base.ref=="main"`; merge только Gitea `POST /pulls/{index}/merge`, никогда push/force-push в
   `main`. Источник истины «слилось» — FR-1.
4. **Регресс-гард целостности `main` (FR-5).** Новая `merge_gate.check_main_regression`,
   вызываемая в `_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done`: проверяет, что
   `origin/main` содержит **декларативный набор маркеров** ключевых функций ранее-merged задач
   (`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → **alert «main
   regressed» + HOLD** (НЕ `done`, БЕЗ авто-отката на `development` — инфра-дефект, ALERT-only как
   ORCH-021/071). Набор — append-only константа `MAIN_REGRESSION_MARKERS` в `merge_gate.py`
   (расширяется каждой значимой задачей). **Fail-open** на git-ошибке самого грепа (регресс
   утверждается только при детерминированном `count==0`); первичный фейл-клозед — SHA-в-main.
   Kill-switch `regression_guard_enabled` (дефолт `true`); non-self → no-op.
5. **`.gitattributes CHANGELOG.md merge=union` (FR-4).** В корне репо; авто-слияние правок
   `## [Unreleased]` без конфликта → `auto_rebase_onto_main` не откатывает ветку и не тащит
   устаревший код-сосед. `docs/**/*.md` под union **НЕ** ставится (union только для append-only;
   доки переписываются построчно).

## Инварианты

never-raise на verify/merge/регресс-гарде (ошибка → alert/HOLD, не падение); прод 8500 не
рестартится/не падает в рамках merge; merge только Gitea PR-API без force-push в `main`; ручной
`Confirm Deploy` (ORCH-059) сохранён; идемпотентность по «SHA-в-main», а не по «любому merged PR»;
non-self репо (enduro) — merge/verify/регресс-гард без изменений. `STAGE_TRANSITIONS`, реестр
`QG_CHECKS`, `check_deploy_status`, схема БД, внешние HTTP-эндпоинты — **без изменений**.

## Альтернативы

- Сохранить PR-флаг как со-критерий verify (с фильтром head/base) — отклонено: PR можно слить и
  тут же откатить ребейзом-соседом; надёжен только факт «SHA в main».
- `docs/**/*.md merge=union` — отклонено: тихая дубликация строк в переписываемых доках.
- Регресс-гард с авто-откатом / хранением маркеров в БД/Plane — отклонено (Не-цель «не менять
  схему БД/Plane»; реакция ALERT-only).
- Fail-closed на marker-grep — отклонено: ложный HOLD при git-сбое; marker-grep вторичен.

## Последствия

Невозможно «`done` + прод задеплоен, а code-PR не в `main`». Ложно-зелёный по docs-PR устранён в
корне. CHANGELOG-конфликты больше не откатывают ветку. Регресс соседнего кода ловится отдельным
гардом. Минус: при недоступной Gitea/git verify консервативно `False` → возможен ложный HOLD+alert
(снимается повтором; fail-closed для `done` приоритетен). Набор маркеров требует дисциплины —
значимая задача дописывает свой маркер.

## Связи

- Amends adr-0013 (ORCH-071), наследует adr-0006 (merge-gate), adr-0011 (job-reaper/lease).
- Детально: `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
