93 lines
7.9 KiB
Markdown
93 lines
7.9 KiB
Markdown
---
|
||
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).
|