--- work_item: ORCH-027 stage: architecture author_agent: architect status: proposed created_at: 2026-06-10 model_used: claude-opus-4-8 --- # adr-0029: Гейт покрытия тестами — edge sub-gate + ratchet-базовая линия - **Статус:** proposed - **Дата:** 2026-06-10 - **Задача:** ORCH-027 - **Детальный ADR:** `docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md` ## Контекст Оркестратор автономен: `developer` пишет код без человека-фильтра, `tester` сам решает, хватает ли тестов. Существующие тестовые гейты судят только по факту прохождения, не по полноте: `check_ci_green` (exit-code CI), `check_tests_passed` (LLM-вердикт `tester`'а), merge-gate re-test (exit-code). Ни один не замечает «300 строк кода, 0 тестов». При пакетном автономном прогоне (ORCH-088) это монотонная деградация покрытия. Нужна детерминированная метрика — по духу как security-гейт (adr-0012). ## Решение Детерминированный (без LLM) **гейт покрытия как под-гейт ребра `deploy-staging → deploy`**, рядом с security-gate (ORCH-022), merge-gate (ORCH-043), image-freshness (ORCH-058). Паттерн — leaf-модуль `src/coverage_gate.py` (never-raise) + обёртка в `QG_CHECKS` (`check_coverage_gate`) + врезка `_handle_coverage_gate` в `advance_stage`. `STAGE_TRANSITIONS` не меняется. - **Порядок: security → merge → `coverage` → image-freshness.** Coverage идёт **ПОСЛЕ merge-gate** (ветка догнана на свежий `origin/main` → меряем покрытие того кода, что landed) и **ДО image-freshness** (фейлить дёшево до docker-rebuild). На этой точке merge-lease **held** → **FAIL обязан освободить lease** при откате (как image-freshness rollback; в отличие от security, который идёт до захвата lease). - **Измеритель:** `pytest-cov` (`coverage.py`), `python -m pytest tests/ --cov=src --cov-report=json` в изолированном worktree (`ensure_worktree`); метрика — `totals.percent_covered`. Тайм-аут `coverage_run_timeout_s`. Скоуп — `src/` (не тесты). - **Чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy, epsilon)`: `absolute` (≥floor−ε), `baseline` (≥baseline−ε, ratchet), `both` (дефолт). `baseline=None` → bootstrap (только absolute). FAIL → откат на `development` + developer-retry (cap `MAX_DEVELOPER_RETRIES`), дословный reason в `task_desc` (ORCH-046). - **Базовая линия — аддитивная БД-таблица** `coverage_baseline(repo PK, coverage, source_sha, updated_at)` (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/`job_deps`). Выбор БД над файлом-в-репо: нет git-churn/конфликтов на ratchet, restart-safe, атомарное обновление. - **Ratchet-up** в choke-point подтверждённого merge `_handle_merge_verify` (ребро `deploy → done`, ORCH-071/073): читает измеренное покрытие из `18-coverage-report.md`, атомарный compare-and-set `UPDATE ... WHERE coverage <= measured` (базовая линия не падает). Под held merge-lease + per-repo сериализацией merge (ORCH-043) — двойная анти-гонка. - **Артефакт `18-coverage-report.md`** с frontmatter `coverage_status: PASS|FAIL` (+ `measured_coverage`/`baseline`/`floor`/`policy`/`delta` + аддитивная 52c-схема); вердикт читается ТОЛЬКО из frontmatter через `src/frontmatter.py` (single source of truth). - **Условность (как ORCH-35/43/58):** `coverage_gate_enabled` + `coverage_gate_repos` (пусто → только self-hosting `orchestrator`); вне области → no-op pass. `applies(repo)` ПЕРВОЙ, дорогой прогон — только при applies. - **Ошибка инструмента → fail-open + WARNING** по умолчанию (`coverage_tool_fail_closed=False`, анти-петля как ORCH-061); флаг → fail-closed. - **Наблюдаемость:** read-only блок `coverage` в `GET /queue`; FAIL → Telegram (кликабельный номер, измеренное/порог/дельта). Опциональный `POST /coverage/baseline` (ручной override). - **never-raise**, гейт не деплоит/не рестартит прод/не пушит в `main` (NFR-3). ## Альтернативы - **CI-job (`check_ci_green`):** пороги/политика/baseline/артефакт плохо выражаются статусом коммита; ratchet требует записи в БД. Отклонено для v1 (точка расширения). - **Edge `testing → deploy-staging`:** ветка не догнана на свежий `main` → метрика неточна; откат не освобождает lease. Отклонено. - **Базовая линия в файле репо:** git-churn/конфликты на каждый ratchet. Отклонено. - **Новая стадия `coverage`:** «пустая» стадия без агента не имеет триггера (как ORCH-043/022). Отклонено. - **Жёсткий absolute-порог без baseline/epsilon:** массовые ложные заворота. Отклонено. ## Последствия - Класс «тихо просевшее покрытие» закрыт детерминированной метрикой; baseline только растёт. - Нулевая регрессия вне области (enduro-trails); `STAGE_TRANSITIONS`/`QG_CHECKS`-семантика/ вердикт-ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`) — байт-в-байт прежние; новая БД-таблица аддитивна. - Плата: ещё один «скрытый» под-гейт ребра; новая pip-зависимость (`pytest-cov`); доп. прогон pytest (после merge-gate re-test, ограничен таймаутом, фейлит до rebuild); v1 — Python-only. - Дефолтный fail-open тихо пропускает при устойчивом сбое инструмента (с WARNING) — переключаемо `coverage_tool_fail_closed`. - Сквозное изменение (новый QG + edge-под-гейт + новая таблица + новый артефакт) → `arch:major-change`; прод-деплой строго через staging-гейт (8501), без рестарта прод-контейнера. - **Откат:** `coverage_gate_enabled=False` → полный no-op (мгновенный обратимый kill-switch). ## Связи adr-0012 (security-гейт — паттерн edge-под-гейта/leaf/never-raise/fail-open), adr-0006 (merge-gate — edge-под-гейт/откат/merge-lease), adr-0008 (image-freshness — условность/ fail-closed/release-lease-on-rollback), adr-0003 (условный гейт / `is_self_hosting_repo`), adr-0009 (анти-петля ложных FAIL, ORCH-061), adr-0013/adr-0014 (merge-verify / SHA-in-main как source of truth — точка ratchet), adr-0015/adr-0017 (per-repo сериализация merge/serial-gate), adr-0020 (frontmatter-контракт — парсинг `coverage_status:`), adr-0019 (PIPELINE_DOCS — артефакт `18-coverage-report.md`), ORCH-9/15 (мульти-стек — будущая зависимость BR-6).