7.9 KiB
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 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 (capMAX_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-setUPDATE ... WHERE coverage <= measured(базовая линия не падает). Под held merge-lease + per-repo сериализацией merge (ORCH-043) — двойная анти-гонка. - Артефакт
18-coverage-report.mdс frontmattercoverage_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-hostingorchestrator); вне области → 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).