Files
orchestrator/docs/architecture/adr/adr-0029-coverage-gate.md

7.9 KiB
Raw Permalink Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-027 architecture architect proposed 2026-06-10 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 heldFAIL обязан освободить 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).