From c26a6b637c2f74b1d0fa9c5be4a9e949cec88efe Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 07:59:49 +0000 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=353 --- docs/work-items/ORCH-071/01-brd.md | 53 +++++++++ docs/work-items/ORCH-071/02-trz.md | 78 +++++++++++++ .../ORCH-071/03-acceptance-criteria.md | 61 +++++++++++ docs/work-items/ORCH-071/04-test-plan.yaml | 103 ++++++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 docs/work-items/ORCH-071/01-brd.md create mode 100644 docs/work-items/ORCH-071/02-trz.md create mode 100644 docs/work-items/ORCH-071/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-071/04-test-plan.yaml diff --git a/docs/work-items/ORCH-071/01-brd.md b/docs/work-items/ORCH-071/01-brd.md new file mode 100644 index 0000000..767f81b --- /dev/null +++ b/docs/work-items/ORCH-071/01-brd.md @@ -0,0 +1,53 @@ +# BRD — ORCH-071: Фантомный merge — деплой без слияния в main + +## 1. Контекст и тип +- **Тип:** BUG CRITICAL (целостность `main` / надёжность деплоя, self-hosting). +- **Обнаружено:** Слава + Стрим, 2026-06-08, при разборе «ORCH-067 не подхватился». +- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`. +- **Подозрение на регресс:** ORCH-065 (idempotent merge / lease-reclaim) — последний честный merge (PR#66). +- **Связано:** восстановление текущего `main` ведётся ОТДЕЛЬНО (ветка `integ/restore-main-2026-06-08`); эта задача — ROOT-FIX, чтобы фантом не повторялся. + +## 2. Проблема (бизнес-формулировка) +Self-deploy (Phase B) для self-hosting репо `orchestrator` собирает прод-образ из ВЕТКИ задачи и рапортует `finalize SUCCESS` + post-deploy `HEALTHY`, **но git-merge ветки в `main` НЕ происходит**. PR остаётся `open`. Следующая задача срезает свою ветку от устаревшего `main` → теряет код незалитых предшественников. + +Накопительно потеряны в `main`: **ORCH-022, 059, 066, 068** (PR#67/68/69/70 — open). Последний реально слитый — ORCH-065 (PR#66). + +## 3. Подтверждённый root cause (по результатам код-аудита) +Гипотеза A постмортема подтверждена аудитом кода ветки: + +1. **В `src/` НЕТ кода, выполняющего merge PR в `main`** (`grep` по `pulls/.../merge`, `/merge`, `merge_pr` — 0 совпадений). Фактический merge выполняет ТОЛЬКО LLM-агент `deployer` через Bash в начале стадии `deploy` (см. `.openclaw/agents/deployer.md`). +2. Для self-hosting (`orchestrator`) стадия `deploy` оркеструется **детерминированным кодом** (`stage_engine._handle_self_deploy_phase_b` → `self_deploy.initiate_deploy` → finalizer `run_deploy_finalizer`), и агент `deployer` **НЕ запускается** (так предписывает `deployer.md`). Detached host-процесс делает retag staging-образа на прод-тег + рестарт 8500. **Ни одна фаза A/B/C не вызывает merge ветки в `main`.** +3. `run_deploy_finalizer` маппит exit-code хука `0→SUCCESS`, пишет `14-deploy-log.md` и вызывает `advance_stage(..., finished_agent="deployer")`. Гейт `check_deploy_status` читает только `deploy_status:` из артефакта → `SUCCESS → done`. **Состояние `main` нигде не верифицируется.** + +Итог: для self-hosting путь `deploy` структурно НЕ содержит шага merge-в-main, а `done` достигается исключительно по deploy-маркеру. «Зелёный» деплой + здоровый прод (образ из рабочей ветки) маскируют отсутствие merge — сигнала о проблеме нет, пока следующая задача не потеряет код предшественника. + +Вторичный фактор (усиливает риск даже если merge добавить наивно): Phase B **рестартит прод-контейнер**, поэтому любой держатель merge-lease / незавершённый git-шаг внутри процесса умирает до завершения merge (урок №3 постмортема). + +## 4. Бизнес-цели +| ID | Цель | +|----|------| +| **G1** | Деплой ВЕРИФИЦИРУЕТ, что задеплоенный commit реально влит в `main` ПОСЛЕ деплоя (deployed SHA — предок `origin/main` ИЛИ `PR.merged==true`). Иначе — alert, задача НЕ `done`. | +| **G2** | Задача → `done` ТОЛЬКО при подтверждённом merge (`PR.merged==true`); маркеров `finalize`/`post-deploy` недостаточно. | +| **G3** | Merge в `main` завершается и подтверждается ДО рестарта прод-контейнера, ЛИБО merge вынесен в шаг, переживающий рестарт (паттерн `requeue_running_jobs` для merge-в-main). | +| **G4** | Диагностический runbook (4 проверки из постмортема) — в `docs/operations`. | + +## 5. Не-цели +- Не менять source-of-truth (Plane), схему БД. +- Не отменять self-hosting safety (no auto-rollback / no-restart-others) — наоборот, усилить верификацией. +- Восстановление текущего `main` (долив 022/059/066/068) — ОТДЕЛЬНАЯ ветка `integ/restore-main-2026-06-08`, вне scope. + +## 6. Инварианты (обязательны к соблюдению) +| ID | Инвариант | +|----|-----------| +| **INV-1** | **never-raise** на шаге верификации — при ошибке шлётся alert, не падение процесса/конвейера. | +| **INV-2** | self-hosting safety: верификация НЕ рестартит и НЕ роняет прод-контейнер `orchestrator` (8500), не трогает другие проекты. | +| **INV-3** | Ручной approve прод-деплоя (триггер «Confirm Deploy», ORCH-059) сохранён — новая логика не вводит авто-деплой. | +| **INV-4** | Никогда не делать force-push / прямой push в `main`; merge только через PR-merge API Gitea (как у deployer-агента сегодня). | +| **INV-5** | Идемпотентность: повторный прогон (re-drive/reaper/двойной webhook) не делает второй merge и не ломает контракты (опора на `pr_already_merged`, ORCH-065). | + +## 7. Заинтересованные стороны +- **Owner** — одобряет прод-деплой («Confirm Deploy»), получает alert при «deployed but not merged». +- **Все проекты на инстансе** (enduro-trails) — косвенно: целостность `main` орка влияет на инструмент, обслуживающий их из общей БД/очереди. + +## 8. Критерий успеха (бизнес-уровень) +После доработки невозможно состояние «задача `done` + прод задеплоен, а PR `open` / commit не в `main`»: либо merge подтверждён и задача `done`, либо задача НЕ `done` и поднят alert «deploy succeeded but not merged». Воспроизведение исходного сценария на staging показывает, что `main` реально получает commit. diff --git a/docs/work-items/ORCH-071/02-trz.md b/docs/work-items/ORCH-071/02-trz.md new file mode 100644 index 0000000..c6b26c0 --- /dev/null +++ b/docs/work-items/ORCH-071/02-trz.md @@ -0,0 +1,78 @@ +# ТЗ — ORCH-071: Верификация merge-в-main как условие done + +> Документ фиксирует ТРЕБОВАНИЯ к изменениям (WHAT). Конкретный дизайн (HOW: новый +> leaf-модуль vs расширение существующего, где разместить шаг merge, формат +> sentinel'ов) — за архитектором (ADR `06-adr/`). ТЗ задаёт инварианты, точки +> врезки и контракты, которые дизайн обязан удовлетворить. + +## 0. Резюме root cause (вход для дизайна) +Для self-hosting (`orchestrator`) стадия `deploy` идёт детерминированным путём +`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`, который +**не содержит шага merge PR в `main`** (merge делает только LLM-`deployer`, не +запускаемый на self-hosting). `done` достигается по `deploy_status: SUCCESS` без +верификации `main`. Требуется: (A) выполнить/докатить merge в `main` детерминированно +до перехода в `done`; (B) верифицировать факт merge ПОСЛЕ деплоя; (C) запретить +`done` без подтверждённого merge. + +## 1. Задействованные модули `src/` +| Модуль | Роль в фиксе | Характер изменения | +|--------|--------------|--------------------| +| `src/stage_engine.py` | `run_deploy_finalizer` (Phase C), терминал-блок `next_stage == "done"`, `_handle_self_deploy_phase_b` | Врезка шага merge-в-main + пост-merge верификация; блокировка перехода в `done` при неподтверждённом merge. | +| `src/merge_gate.py` | Уже содержит `pr_already_merged` (ORCH-065, read-only guard) | Добавить детерминированный **merge-актор** для self-hosting (выполнить merge PR через Gitea API) + helper верификации «SHA предок `origin/main`». Опора на существующие `pid_alive`/`reclaim_stale_lease`. | +| `src/self_deploy.py` | Sentinel-state Phase A/B/C | Возможный новый sentinel-маркер `merged` (restart-safe), если дизайн выносит merge в отдельный переживающий рестарт шаг (G3). | +| `src/qg/checks.py` | Реестр `QG_CHECKS`, `check_deploy_status` | Возможный новый под-чек верификации merge (например `check_merged_to_main`) ЛИБО усиление условия перехода `deploy→done`. `check_deploy_status` НЕ менять по контракту парсинга. | +| `src/config.py` | Флаги | Новый kill-switch (напр. `merge_verify_enabled` / `merge_verify_repos`), таймауты merge/verify. Дефолт — область self-hosting (как ORCH-35/43/58). | +| `.openclaw/agents/deployer.md` | Промпт deployer'а (non-self merge) | Уточнить: для self-hosting merge выполняет детерминированный код; non-self путь без изменений. | +| `src/main.py` (`/queue`) | Наблюдаемость | Опционально: блок/счётчики верификации merge (`merge_verified_total`, `not_merged_alerts_total`). | + +## 2. Функциональные требования + +### FR-1 (G3) — Детерминированный merge-в-main для self-hosting +- Для self-hosting репо merge PR ветки в `main` ДОЛЖЕН выполняться **детерминированным кодом** (не LLM-агентом), т.к. `deployer`-агент на self-hosting `deploy` не запускается. +- Merge выполняется через **Gitea PR-merge API** (как сегодня делает агент), НИКОГДА не force-push / не прямой push в `main` (INV-4). +- ПЕРЕД merge консультироваться `merge_gate.pr_already_merged(repo, branch)` — уже слит → no-op (INV-5, переиспользовать ORCH-065). +- **G3 — порядок относительно рестарта:** merge ДОЛЖЕН быть завершён и подтверждён ДО рестарта прод-контейнера, ЛИБО вынесен в шаг, переживающий рестарт (паттерн `requeue_running_jobs`/finalizer-defer): если процесс умер во время Phase B, шаг merge докатывается после рестарта (re-drive finalizer'а или отдельный merge-job). Дизайн выбирает один из двух вариантов; выбранный обязан быть restart-safe (sentinel/jobs, без миграции БД — §4). + +### FR-2 (G1) — Пост-деплой верификация merge +- ПОСЛЕ деплоя (в Phase C / финализации, ДО фиксации `done`) выполнить детерминированную верификацию: задеплоенный commit (validated SHA) — **предок `origin/main`** (`git merge-base --is-ancestor origin/main`) **ИЛИ** `PR.merged == true` (Gitea API). +- Верификация **never-raise** (INV-1): любая ошибка git/HTTP → трактуется как «не подтверждено» → alert, НЕ падение. +- При неподтверждённой верификации — **alert** «deploy succeeded but not merged» (Telegram + Plane-коммент) и задача **НЕ переходит в `done`** (FR-3). + +### FR-3 (G2) — `done` только при подтверждённом merge +- Переход `deploy → done` для self-hosting ДОЛЖЕН быть обусловлен подтверждённым merge (verify из FR-2 зелёный). Наличие `deploy_status: SUCCESS` + post-deploy `HEALTHY` — **недостаточно**. +- При `SUCCESS`-маркере деплоя, но неподтверждённом merge: задача удерживается (не `done`), Plane-статус — не терминальный (например текущий `Deploying`/`Awaiting` или `Blocked` по решению дизайна), шлётся alert. Конвейер НЕ откатывается на `development` автоматически из-за not-merged (это инфраструктурный, не код-дефект) — реакция = alert + ручное вмешательство (согласовать с дизайном; по умолчанию ALERT-only, как ORCH-021 self-hosting). + +### FR-4 (G4) — Диагностический runbook +- В `docs/operations/` добавить runbook с 4 проверками из постмортема (метод однозначной локализации фантома): + 1. Gitea API: список PR + флаги `merged`. + 2. md5 прод-файлов vs `git show origin/main:`. + 3. `git merge-base` ветки vs `main`. + 4. Таймлайн деплой-логов. +- Включить готовые команды (copy-paste) и критерий «фантом подтверждён». + +### FR-5 — Условность раската (как ORCH-35/43/58) +- Новая логика merge+verify реальна для self-hosting (`is_self_hosting_repo` / `merge_verify_repos`); прочие репо — поведение БЕЗ изменений (non-self merge остаётся за агентом `deployer`). +- Kill-switch (env, дефолт `true`) → `false` восстанавливает строго прежнее поведение. + +## 3. Изменения API +- **Внешний HTTP API сервиса (`/health`, `/status`, `/queue`, `/webhook/*`) — без новых endpoint'ов.** Допустимо обогащение ответа `GET /queue` блоком наблюдаемости merge-verify (счётчики), по образцу блоков `reaper`/`post_deploy`. +- **Gitea API (исходящие вызовы):** новый детерминированный вызов `POST /repos/{owner}/{repo}/pulls/{index}/merge` (merge-актор, FR-1) + чтение `GET /repos/{owner}/{repo}/pulls?...` (уже используется в `pr_already_merged`). Через существующий httpx-клиент и `settings.gitea_*`. + +## 4. Изменения схемы БД +- **НЕТ.** Schema-changes запрещены (не-цель). Restart-safe состояние нового шага merge — через sentinel-файлы (`.deploy-state-//`, как ORCH-036) и/или существующую очередь `jobs` (finalizer-defer). Колонка `jobs.pid` (ORCH-065) уже есть, при необходимости переиспользуется. + +## 5. Требования к новым QG checks +- Допускается ввести детерминированный под-чек верификации merge (напр. `check_merged_to_main`), регистрируемый в `QG_CHECKS`, ЛИБО встроить верификацию как условие в логику перехода `deploy→done` без нового чека — на усмотрение дизайна. В любом случае: + - Контракт `check_deploy_status` / `_parse_deploy_status` (читает только `deploy_status:` frontmatter) **НЕ меняется**. + - `STAGE_TRANSITIONS` **НЕ меняется** (verify — это условие/под-гейт ребра/финализации, не новая стадия). + - Вердикт (если артефакт) — строго YAML-frontmatter (канон гейтов), never проза. + +## 6. Артефакты, создаваемые/обновляемые по pipeline +- `14-deploy-log.md` — существующий; дизайн может добавить поле статуса merge (напр. `merged_to_main: true|false`) во frontmatter (машиночитаемо), не ломая `deploy_status:`. +- Новый runbook в `docs/operations/` (FR-4). +- **Обязательно (CLAUDE.md §2):** обновить `docs/architecture/README.md` (раздел Phase B / merge-gate / executable self-deploy — описать новый merge+verify шаг), `CHANGELOG.md`, при сквозном решении — ADR (`docs/work-items/ORCH-071/06-adr/ADR-001-*.md` и/или global `docs/architecture/adr/`). + +## 7. Совместимость / регресс +- Happy-path не-self репо (enduro-trails): merge остаётся за агентом `deployer` → поведение без изменений. +- Happy-path self-hosting: при штатном merge задача `done` ставится как раньше (после добавления verify, который зелёный). +- Все существующие контракты неизменны: `STAGE_TRANSITIONS`, реестр `QG_CHECKS` (кроме возможного нового under-чека), `check_deploy_status`, БАГ-8, terminal-sync, merge-gate (ORCH-043), `Confirm Deploy` (ORCH-059), exit-коды хука (0/1/2), схема БД. diff --git a/docs/work-items/ORCH-071/03-acceptance-criteria.md b/docs/work-items/ORCH-071/03-acceptance-criteria.md new file mode 100644 index 0000000..1de080c --- /dev/null +++ b/docs/work-items/ORCH-071/03-acceptance-criteria.md @@ -0,0 +1,61 @@ +# Критерии приёмки — ORCH-071 + +Формат: каждый критерий имеет явное условие PASS/FAIL. Машинные вердикты — из артефактов/состояния, не из прозы. + +## AC-1 (G1) — Пост-деплой верификация: not-merged ⇒ не done + alert +- **Условие:** после Phase B/финализации, если задеплоенный commit НЕ влит в `origin/main` (не предок `origin/main` И `PR.merged != true`). +- **PASS:** задача НЕ переходит в `done`; шлётся alert «deploy succeeded but not merged» (Telegram + Plane-коммент). +- **FAIL:** задача стала `done` при неслитом PR ИЛИ alert не отправлен. + +## AC-2 (G2) — done только при PR.merged==true (mock-тест) +- **Условие:** SUCCESS-маркеры деплоя присутствуют (`deploy_status: SUCCESS`), но PR `open` (`merged=false`). +- **PASS:** переход в `done` НЕ выполняется (тест на mock Gitea: PR open → done не ставится). +- **FAIL:** задача переведена в `done`. + +## AC-3 (G3) — Merge подтверждён до/независимо от рестарта (smoke) +- **Условие:** симулирован рестарт контейнера во время Phase B (процесс/держатель merge умер до завершения merge). +- **PASS:** после рестарта merge докатывается (re-drive finalizer / merge-job, как `requeue_running_jobs`), `main` получает commit, верификация зелёная → задача `done`. +- **FAIL:** после рестарта merge не докатился, задача `done` без merge ИЛИ навсегда зависла без alert. + +## AC-4 (регресс) — Happy-path +- **Условие:** merge прошёл штатно, `PR.merged==true`, deploy `SUCCESS`, верификация зелёная. +- **PASS:** `done` ставится как раньше (терминал-sync/Plane-статус как сегодня для self-hosting), без лишних alert. +- **FAIL:** регрессия — happy-path не доходит до `done` или шлёт ложный not-merged alert. + +## AC-4b (регресс) — non-self репо без изменений +- **Условие:** деплой репо enduro-trails (не self-hosting). +- **PASS:** merge выполняет агент `deployer` (прежний путь), новая детерминированная merge/verify-логика — no-op для не-self. +- **FAIL:** изменилось поведение non-self деплоя. + +## AC-5 — Зелёный pytest + документация +- **PASS:** `pytest tests/ -q` зелёный; обновлены `CHANGELOG.md`, `docs/architecture/README.md` (раздел Phase B / merge-verify) и runbook (`docs/operations/`). +- **FAIL:** красные тесты ИЛИ документация/CHANGELOG/runbook не обновлены (reviewer → REQUEST_CHANGES, CLAUDE.md §6). + +## AC-6 — Воспроизведение исходного сценария на staging +- **Условие:** на staging провести задачу до деплоя. +- **PASS:** проверить (методом runbook), что `main` реально получил commit задачи (PR merged / SHA предок `origin/main`). +- **FAIL:** прод/«done» достигнуты, а `main` не получил commit. + +## AC-7 (INV-1) — never-raise на верификации +- **Условие:** verify сталкивается с ошибкой git/HTTP (Gitea недоступна, битый ref). +- **PASS:** функция возвращает «не подтверждено» → alert, процесс/конвейер НЕ падает (исключение не пробрасывается). +- **FAIL:** исключение из verify валит finalizer/advance_stage. + +## AC-8 (INV-2) — self-hosting safety +- **Условие:** шаг верификации/merge исполняется для `orchestrator`. +- **PASS:** verify/merge НЕ рестартят и НЕ роняют прод-контейнер 8500, не трогают другие проекты; merge — только PR-merge API, без push в `main`. +- **FAIL:** verify/merge перезапускает прод ИЛИ делает прямой/force push в `main`. + +## AC-9 (INV-5) — идемпотентность повторного прогона +- **Условие:** re-drive стадии `deploy` / повторный webhook / reaper-requeue при уже слитом PR. +- **PASS:** `pr_already_merged` → merge не повторяется (no-op), верификация зелёная, нет дубль-merge/ошибки Gitea, нет ложного БАГ-8 отката. +- **FAIL:** второй merge / merge-error / ложный откат. + +## AC-10 (FR-5) — kill-switch +- **Условие:** kill-switch новой merge/verify-логики выключен (`false`). +- **PASS:** строго прежнее поведение (1:1 до фикса). +- **FAIL:** при выключенном флаге логика всё равно срабатывает. + +## AC-11 (INV-3) — ручной approve сохранён +- **PASS:** прод-деплой по-прежнему запускается только статусом «Confirm Deploy» (ORCH-059); merge/verify не вводят авто-деплой. +- **FAIL:** деплой/merge запускается без человеческого триггера. diff --git a/docs/work-items/ORCH-071/04-test-plan.yaml b/docs/work-items/ORCH-071/04-test-plan.yaml new file mode 100644 index 0000000..af5de7f --- /dev/null +++ b/docs/work-items/ORCH-071/04-test-plan.yaml @@ -0,0 +1,103 @@ +work_item: ORCH-071 +title: "Верификация merge-в-main как условие done (фантомный merge)" +notes: > + Тесты детерминированные, без LLM. Gitea/PR-состояние и git-операции мокаются + (monkeypatch httpx / subprocess / merge_gate helpers). Цель — закрыть AC-1..AC-11. + Все новые функции верификации/merge соблюдают never-raise. + +tests: + # --- FR-2 / G1 / AC-1: пост-деплой верификация merge --- + - id: TC-01 + type: unit + description: "verify_merged_to_main возвращает True, когда deployed SHA — предок origin/main (git merge-base --is-ancestor rc=0)" + module: tests/test_merge_verify.py + expected: PASS + - id: TC-02 + type: unit + description: "verify_merged_to_main возвращает True, когда PR.merged==true (Gitea mock), даже если git-проверка недоступна" + module: tests/test_merge_verify.py + expected: PASS + - id: TC-03 + type: unit + description: "verify_merged_to_main возвращает False, когда SHA не предок origin/main И PR.merged==false (фантом)" + module: tests/test_merge_verify.py + expected: PASS + - id: TC-04 + type: unit + description: "never-raise (AC-7): ошибка git/HTTP в verify -> False (не подтверждено), исключение не пробрасывается" + module: tests/test_merge_verify.py + expected: PASS + + # --- FR-3 / G2 / AC-2: done только при подтверждённом merge --- + - id: TC-05 + type: integration + description: "Phase C finalizer: deploy_status=SUCCESS но PR open -> задача НЕ переходит в done, шлётся alert 'deploy succeeded but not merged'" + module: tests/test_deploy_finalizer_merge_gate.py + expected: PASS + - id: TC-06 + type: integration + description: "Phase C finalizer: deploy_status=SUCCESS и merge подтверждён -> задача переходит в done (happy-path, AC-4)" + module: tests/test_deploy_finalizer_merge_gate.py + expected: PASS + + # --- FR-1 / AC-9: детерминированный merge-актор + идемпотентность --- + - id: TC-07 + type: unit + description: "merge-актор self-hosting вызывает Gitea POST /pulls/{index}/merge, когда PR не слит; никакого push/force-push в main" + module: tests/test_merge_actor.py + expected: PASS + - id: TC-08 + type: unit + description: "идемпотентность (AC-9): pr_already_merged==True -> merge-актор no-op (нет второго merge, нет ошибки Gitea)" + module: tests/test_merge_actor.py + expected: PASS + - id: TC-09 + type: unit + description: "merge-актор never-raise: ошибка Gitea API -> (False, reason), исключение не пробрасывается" + module: tests/test_merge_actor.py + expected: PASS + + # --- FR-1 G3 / AC-3: merge переживает рестарт --- + - id: TC-10 + type: integration + description: "smoke (AC-3): симуляция смерти процесса во время Phase B -> re-drive finalizer/merge-job докатывает merge после 'рестарта', main получает commit, verify зелёная -> done" + module: tests/test_deploy_restart_merge_recovery.py + expected: PASS + + # --- FR-5 / AC-10: условность раската --- + - id: TC-11 + type: unit + description: "AC-4b: для non-self репо (enduro-trails) новая merge/verify-логика = no-op (merge остаётся за агентом deployer)" + module: tests/test_merge_verify.py + expected: PASS + - id: TC-12 + type: unit + description: "AC-10: kill-switch выключен -> строго прежнее поведение (verify/merge не выполняются)" + module: tests/test_merge_verify.py + expected: PASS + + # --- INV-2 / AC-8: self-hosting safety --- + - id: TC-13 + type: unit + description: "AC-8: путь merge/verify не вызывает рестарт прод-контейнера и не делает прямой/force push в main (проверка отсутствия соответствующих вызовов)" + module: tests/test_merge_actor.py + expected: PASS + + # --- INV-3 / AC-11: ручной approve сохранён --- + - id: TC-14 + type: integration + description: "AC-11: Phase B запускается только при confirm_deploy=True ('Confirm Deploy'); merge/verify не вводят авто-деплой (обычный Approved -> no-op)" + module: tests/test_deploy_finalizer_merge_gate.py + expected: PASS + + # --- Регресс существующих контрактов --- + - id: TC-15 + type: unit + description: "регресс: check_deploy_status / _parse_deploy_status неизменны (читают только deploy_status: frontmatter)" + module: tests/test_qg_checks.py + expected: PASS + - id: TC-16 + type: unit + description: "регресс: STAGE_TRANSITIONS и реестр QG_CHECKS не сломаны (deploy->done ребро на месте)" + module: tests/test_stages.py + expected: PASS