# Runbook — диагностика «фантомного merge» (ORCH-071)

> **Когда применять.** Задача дошла до `done` (или прод задеплоен «зелёным»), но есть
> подозрение, что её ветка **не влита в `main`** — следующая задача срежет ветку от
> устаревшего `main` и потеряет код предшественника (постмортем
> `docs/history/LESSONS_2026-06-08_phantom-merge.md`). Этот runbook даёт 4 проверки
> для **однозначной локализации** фантома.

С ORCH-071 такой исход блокируется автоматически: под-гейт `deploy → done`
(`stage_engine._handle_merge_verify`) сначала **детерминированно вливает PR**
(`merge_gate.merge_pr`, Gitea PR-merge API), затем **верифицирует merge**
(`merge_gate.verify_merged_to_main`) и НЕ пускает задачу в `done`, пока merge не
подтверждён (alert + HOLD). Этот runbook — для ручной перепроверки/инцидентов
(в т.ч. при выключенном kill-switch `ORCH_MERGE_VERIFY_ENABLED=false`).

Подставьте значения:

```bash
OWNER=admin                       # settings.gitea_owner
REPO=orchestrator                 # репозиторий
BRANCH=feature/ORCH-071-slug      # ветка задачи
GITEA=http://localhost:3000       # settings.gitea_url
TOKEN=<gitea_token>               # settings.gitea_token
FILE=src/stage_engine.py          # любой файл, гарантированно изменённый задачей
```

---

## Проверка 1 — Gitea API: список PR + флаги `merged`

Показывает, считает ли сам Gitea PR влитым.

```bash
curl -s -H "Authorization: token $TOKEN" \
  "$GITEA/api/v1/repos/$OWNER/$REPO/pulls?state=all" \
  | python3 -c 'import sys,json; \
[print(p["number"], p["state"], "merged="+str(p.get("merged")), p["head"]["ref"]) \
 for p in json.load(sys.stdin)]'
```

* **Фантом НЕ подтверждён (всё хорошо):** строка ветки `$BRANCH` имеет `merged=True`.
* **Фантом подтверждён (по этому критерию):** PR ветки `state=open` / `merged=False`
  (или PR отсутствует), при том что задача в `done` / прод задеплоен.

---

## Проверка 2 — md5 прод-файлов vs `git show origin/main:<file>`

Сверяет содержимое файла на проде с тем, что лежит в `origin/main`.

```bash
# в прод-контейнере (или через docker exec orchestrator):
md5sum "/app/$FILE"

# содержимое того же файла из origin/main (на хосте, в клоне репо):
git -C /home/slin/repos/$REPO fetch origin main -q
git -C /home/slin/repos/$REPO show "origin/main:$FILE" | md5sum
```

* **Совпало:** прод соответствует `main` (фантома нет ИЛИ задача не меняла этот файл —
  возьмите файл из проверки 3/diff'а ветки).
* **Разошлось:** прод собран из ветки, а `main` его не получил → косвенный признак фантома.

---

## Проверка 3 — `git merge-base` ветки vs `main`

Главный детерминированный критерий: является ли HEAD ветки предком `origin/main`.

```bash
git -C /home/slin/repos/$REPO fetch origin -q
SHA=$(git -C /home/slin/repos/$REPO rev-parse "origin/$BRANCH")
git -C /home/slin/repos/$REPO merge-base --is-ancestor "$SHA" origin/main \
  && echo "MERGED: ветка влита в main" \
  || echo "NOT MERGED: ветка НЕ предок origin/main (ФАНТОМ)"
```

Это ровно та проверка, что выполняет `merge_gate.verify_merged_to_main` (rc=0 → влито).

* **`MERGED`:** фантома нет.
* **`NOT MERGED`:** фантом подтверждён — `main` не содержит коммитов задачи.

---

## Проверка 4 — таймлайн деплой-логов

Восстанавливает порядок событий: был ли merge до/после деплоя, и был ли он вообще.

```bash
# Вердикт деплоя + новое поле merge-верификации (ORCH-071):
git -C /home/slin/repos/$REPO show "origin/$BRANCH:docs/work-items/<WI>/14-deploy-log.md" \
  | sed -n '1,12p'   # frontmatter: deploy_status:, merged_to_main:

# Наблюдаемость под-гейта в живом сервисе:
curl -s "$GITEA_HEALTH/queue" | python3 -c \
  'import sys,json; print(json.load(sys.stdin)["merge_verify"])'
# -> {"enabled":..., "merge_verified_total":..., "not_merged_alerts_total":..., "last_alert_wi":...}

# Журнал хоста по деплою (sentinel-каталог задачи):
ls -la /home/slin/repos/.deploy-state-$REPO/<WI>/
cat   /home/slin/repos/.deploy-state-$REPO/<WI>/hook.log
```

* `deploy_status: SUCCESS` + `merged_to_main: false` → деплой прошёл, merge — нет
  (это и есть класс ORCH-071; задача должна быть удержана на `deploy`, не `done`).
* `not_merged_alerts_total` растёт / `last_alert_wi == <WI>` → под-гейт уже поднял alert.

---

## Критерий «фантом подтверждён»

Фантомный merge считается **подтверждённым**, если выполняется ХОТЯ БЫ ОДНО из:

1. Проверка 1: PR ветки `state=open` / `merged=False` (или PR нет), а задача в `done`.
2. Проверка 3: `merge-base --is-ancestor` вернул **NOT MERGED** (HEAD ветки не предок `origin/main`).
3. Проверка 4: `14-deploy-log.md` имеет `deploy_status: SUCCESS` при `merged_to_main: false`.

Проверка 2 — вспомогательная (зависит от того, менял ли файл задачей), используется
для подтверждения проверок 1/3.

### Что делать при подтверждённом фантоме

1. **Влить PR вручную** через Gitea (PR-merge API / UI) — НИКОГДА не `git push`/`--force` в `main` (INV-4).
2. Повторить approve задачи (re-drive) — под-гейт переоценит: merge подтвердится → задача уйдёт в `done`.
3. Если фантом случился при выключенном kill-switch — включить `ORCH_MERGE_VERIFY_ENABLED=true`.
