--- work_item: ORCH-027 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-10 model_used: claude-opus-4-8 --- # 01 — BRD (бизнес-требования): ORCH-027 — Code coverage как гейт (защита от деградации покрытия тестами) Work Item: **ORCH-027** · Repo: **orchestrator** · Стадия: analysis ## 1. Бизнес-контекст и проблема Оркестратор ведёт **автономную** разработку: код пишет агент `developer` без человеческого фильтра, а на стадии `testing` агент `tester` сам решает, достаточно ли тестов. Существующие тестовые гейты проверяют только **факт прохождения** тестов, а не их **полноту**: - `check_ci_green` (ребро `development → review`) — зелёный прогон `pytest tests/` в Gitea CI (`.gitea/workflows/ci.yml`), судит по exit-code, покрытие **не меряет**. - `check_tests_passed` (ребро `testing → deploy-staging`) — читает machine-verdict `result:`/`verdict:`/`status:` из `13-test-report.md`; это вердикт LLM-`tester`'а, а не измеренная метрика. - Merge-gate re-test (ORCH-043) — повторный `pytest` на догнанной ветке, тоже только exit-code. Ни один гейт не замечает, что фича добавила 300 строк кода и 0 тестов, или что багфикс изменил поведение без регрессионного теста. При пакетном автономном прогоне (эпик ORCH-088, «10–20 задач за ночь») это означает **монотонную деградацию покрытия**: каждая задача может «срезать угол» на тестах, и за десятки задач проект тихо теряет тестируемость. Предложено Стрим, одобрено Славой (`00-business-request.md`). **Задача вводит измеримый гейт покрытия**: покрытие тестами измеряется инструментально и не должно опускаться ниже политики (абсолютный порог и/или «не ниже базовой линии»). Это структурная защита от деградации, аналогичная по духу security-гейту (ORCH-022) — детерминированная метрика вместо доверия суждению агента. > **Self-hosting.** Гейт работает на инструменте, который в проде обслуживает все проекты из > общей БД и очереди (`CLAUDE.md` §self-hosting). Измерение покрытия — это исполнение тест-сьюта > в изолированном worktree; оно **не трогает прод-контейнер и не касается `main`**. ## 2. Объём (scope) ### В объёме - Инструментальное измерение покрытия тестами для репозитория `orchestrator` (стек Python / pytest) перед слиянием ветки задачи в `main`. - Гейт-решение: покрытие **не ниже** заданной политики порога. Политика поддерживает два режима: абсолютный порог (`%`) и «не ниже базовой линии» (no-regression / ratchet), а также их комбинацию. - Хранение и обновление **базовой линии** покрытия (last-known покрытие `main`). - Наблюдаемость результата: артефакт-отчёт о покрытии с machine-readable вердиктом, строка в `GET /queue`, сигнал в Telegram при провале. - Конфигурируемость: kill-switch + per-repo область + настраиваемый порог/политика + поведение при ошибке инструмента (fail-open/closed). ### Вне объёма - Реализация измерения покрытия для НЕ-Python стеков (jest / jacoco для будущих репозиториев) — фактическая интеграция инструментов оставлена на будущее; в ORCH-027 закладывается лишь расширяемость (политика и хранилище не должны быть жёстко завязаны на Python). - Изменение существующей семантики `check_ci_green` / `check_tests_passed` / `check_reviewer_verdict` для репозиториев, где гейт покрытия выключен. - Принудительное доведение покрытия до 100% или установка агрессивного абсолютного порога — стартовая политика консервативна (см. NFR-4). - Покрытие самих тестовых файлов и мутационное тестирование. - Выбор конкретного инструмента/механизма интеграции и его расположения в конвейере как архитектурного решения — это зона архитектора (`06-adr/`); BRD/ТЗ фиксируют требования и кандидатные точки, выведенные из фактического кода. ## 3. Заинтересованные стороны - **Заказчик / инициатор:** Стрим (предложение), Слава (одобрение). - **Затрагиваются:** конвейер `orchestrator` (self-hosting); агенты `developer`/`tester` (теперь обязаны держать покрытие); проект enduro-trails — **не должен быть затронут** (гейт по умолчанию неактивен вне сконфигурированных репозиториев). - **Принимает результат:** reviewer (стадия `review`) + финальная стадия конвейера; владелец (Owner) — по факту работы гейта в проде. ## 4. Бизнес-требования (BR) - **BR-1 — Измерение покрытия.** Перед слиянием ветки задачи в `main` покрытие тестами репозитория измеряется инструментально (исполнением тест-сьюта под coverage-инструментацией), а не оценивается на глаз. Результат — числовая метрика покрытия (как минимум line coverage). - **BR-2 — Гейт деградации.** Если измеренное покрытие нарушает политику (ниже абсолютного порога ИЛИ ниже базовой линии — в зависимости от выбранного режима), конвейер **не пропускает** задачу дальше к деплою и инициирует штатный откат на `development` для доработки тестов. - **BR-3 — Базовая линия (ratchet).** Поддерживается режим «не ниже предыдущего»: гейт сравнивает покрытие ветки с зафиксированной базовой линией `main`. Базовая линия **обновляется вверх** при успешном слиянии задачи в `main` (покрытие может только расти или держаться, но не падать). - **BR-4 — Конфигурируемость и нулевая регрессия.** Гейт управляется kill-switch'ем и per-repo областью (по образцу `merge_gate`/`security_gate`/`image_freshness`, ORCH-035/043/058). Для репозиториев вне области (в частности enduro-trails) гейт — **полный no-op**, поведение конвейера 1:1 как до задачи. Порог, политика (absolute|baseline|both) и поведение при ошибке инструмента — настраиваемы. - **BR-5 — Наблюдаемость.** Результат измерения виден: (а) артефакт-отчёт о покрытии с machine-readable вердиктом в `docs/work-items//`; (б) read-only блок в `GET /queue`; (в) уведомление в Telegram при провале гейта (кликабельный номер задачи, как у прочих алертов). Сообщение указывает измеренное покрытие, порог/базовую линию и дельту. - **BR-6 — Стек-расширяемость.** Логика политики (PASS/FAIL по метрике/базовой линии) и хранилище базовой линии не зависят от конкретного инструмента; добавление измерителя для другого стека (jest/jacoco) в будущем не требует переписывания ядра гейта. ## 5. Нефункциональные требования (NFR) - **NFR-1 — never-raise / fail-safe.** Ядро гейта — изолированный leaf-модуль (по образцу `src/security_gate.py`, `src/serial_gate.py`, `src/labels.py`): любая внутренняя ошибка обрабатывается, исключение **никогда** не всплывает в `advance_stage` и не роняет конвейер всех проектов. - **NFR-2 — Поведение при недоступности/ошибке инструмента.** По умолчанию ошибка измерения (coverage-инструмент упал/недоступен) → **fail-open + громкий warning** (анти-петля, прецедент ORCH-061/ORCH-022 dep-audit), переключаемое в fail-closed флагом. Дефолт не должен заклинивать автономный конвейер из-за инфраструктурного сбоя. - **NFR-3 — Self-hosting безопасность.** Гейт только исполняет тесты в изолированном worktree, читает метрику, пишет отчёт и принимает решение. Он **никогда** не вызывает деплой-хук, не перезапускает прод-контейнер, не пушит/форс-пушит в `main`. - **NFR-4 — Консервативный старт (анти-флап).** Стартовая политика не должна массово заворачивать существующие задачи: базовая линия инициализируется фактическим покрытием `main`, абсолютный порог — как мягкий backstop. Допускается малый отрицательный допуск (epsilon) на шум измерения, чтобы дрожание ±доли процента не заворачивало задачу. - **NFR-5 — Совместимость.** `STAGE_TRANSITIONS`, состав/семантика `QG_CHECKS` и `check_*`, machine-verdict ключи существующих доков (`verdict:`/`result:`/`deploy_status:`/ `staging_status:`/`security_status:`) — не меняются. Любая новая БД-сущность — аддитивна (без миграции существующих таблиц). Restart-safe. - **NFR-6 — Детерминизм.** Решение гейта — чистая функция от (измеренное покрытие, базовая линия, порог, политика); без участия LLM в критическом пути (как security/merge/image-freshness под-гейты). ## 6. Допущения и ограничения - Тест-сьют `orchestrator` запускается командой `python -m pytest tests/` из корня репозитория (подтверждено `.gitea/workflows/ci.yml`, `pytest.ini` `testpaths = tests`); измерение покрытия накладывается на этот же прогон. - Coverage-инструмент для Python (`coverage.py` / `pytest-cov`) добавляется как pip-зависимость; он не требует сети во время измерения. - Репозиторий `orchestrator` — единственный self-hosting (предикат `is_self_hosting_repo`); стартовая область гейта — он. enduro-trails и прочие репозитории по умолчанию вне области. - Базовая линия привязана к покрытию `main`; её первичная инициализация выполняется один раз (bootstrap) фактическим замером текущего `main`. - Тесты исполняются в per-branch worktree (`ensure_worktree`), что безопасно при параллельных активных задачах (прецедент `check_tests_local`/merge-gate re-test). ## 7. Критерии успеха - Покрытие тестами `orchestrator` измеряется на каждой задаче и не может опуститься ниже политики, не заблокировав продвижение к деплою. - При выключенном флаге / вне области — конвейер ведёт себя 1:1 как до ORCH-027 (нулевая регрессия для enduro-trails). - Сбой coverage-инструмента не заклинивает автономный конвейер (дефолт fail-open + warning). - Результат измерения прозрачен (отчёт + `GET /queue` + Telegram при провале). Детальные PASS/FAIL — `03-acceptance-criteria.md`. ## 8. Риски - **Флап на шуме измерения** — недетерминированное покрытие (например, зависящее от порядка/ окружения) может дрожать у границы → ложные заворота. Митигировать epsilon-допуском (NFR-4). - **Петля заворотов** — слишком высокий абсолютный порог завернёт многие задачи в бесконечный rework. Митигировать консервативной стартовой политикой и baseline-режимом. - **Гонка базовой линии** при параллельных слияниях — два слияния в `main` могут конкурентно обновлять baseline. Требуется атомарное/сериализованное обновление (опереться на окно сериализации merge-lease, ORCH-043). - **Инфраструктурная хрупкость** — coverage-инструмент недоступен/несовместим с версией pytest → закрыто требованием NFR-2 (fail-open + warning). Детальная техническая проработка рисков — `10-tech-risks.md` (заполняет архитектор).