---
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/<id>/`; (б) 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` (заполняет архитектор).
