157 lines
15 KiB
Markdown
157 lines
15 KiB
Markdown
---
|
||
work_item: ORCH-027
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-10
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 02 — ТЗ (TRZ): ORCH-027 — Code coverage как гейт
|
||
|
||
Work Item: **ORCH-027** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> ТЗ описывает **конкретные требования к реализации**, выведенные из BRD и фактического кода.
|
||
> Архитектурное обоснование и выбор механизма (где именно врезать гейт, как хранить базовую
|
||
> линию, какой инструмент) — задача архитектора (`06-adr/`). Ниже зафиксированы требования и
|
||
> **кандидатные** точки интеграции, грунтованные реальным кодом; финальное решение по каждой
|
||
> отмеченной точке принимает архитектор.
|
||
|
||
## 1. Сводка изменения
|
||
|
||
Вводится **детерминированный гейт покрытия тестами** для репозитория `orchestrator`. Гейт
|
||
измеряет покрытие исполнением тест-сьюта под coverage-инструментацией, сравнивает с политикой
|
||
(абсолютный порог и/или базовая линия `main`) и блокирует продвижение задачи к деплою при
|
||
деградации, инициируя штатный откат на `development`. Ядро — изолированный leaf-модуль с чистой
|
||
логикой решения (по образцу `security_gate`/`serial_gate`), управляемый kill-switch'ем и per-repo
|
||
областью; вне области — полный no-op. Базовая линия покрытия `main` хранится персистентно и
|
||
обновляется вверх при слиянии (ratchet).
|
||
|
||
## 2. Задействованные модули / пути
|
||
|
||
| Путь | Действие | Назначение |
|
||
|------|----------|-----------|
|
||
| `requirements.txt` | изменить | добавить coverage-зависимость Python (`coverage.py` / `pytest-cov`; точный выбор — архитектор) |
|
||
| `src/coverage_gate.py` | создать | **NEW leaf-модуль**: измерение покрытия (run suite под coverage в `ensure_worktree`), чистые функции `compute_coverage_verdict(measured, baseline, floor, policy, epsilon)` и классификация, чтение/запись отчёта; never-raise; импортирует только `config`/`git_worktree` (+ лениво `qg.checks.is_self_hosting_repo`/`notifications`) |
|
||
| `src/config.py` | изменить | добавить флаги гейта (см. §6 ниже / раздел совместимости) |
|
||
| `src/qg/checks.py` | изменить | зарегистрировать механизм проверки покрытия (новый `check_*` ЛИБО делегирование из под-гейта); **семантика существующих `check_*` не меняется** |
|
||
| `src/stage_engine.py` | изменить *(кандидат)* | врезка под-гейта в `advance_stage` по образцу `_handle_security_gate`/`_handle_merge_gate` — если выбран механизм «edge sub-gate» (см. §3 FR-3) |
|
||
| `src/db.py` | изменить *(кандидат)* | аддитивная таблица базовой линии покрытия (`coverage_baseline` per-repo), если базовая линия хранится в БД, а не в файле; `_ensure_column`/`CREATE TABLE IF NOT EXISTS` — без миграции существующих |
|
||
| `.gitea/workflows/ci.yml` | изменить *(кандидат)* | если измерение делается в CI-шаге — добавить `--cov`/порог в прогон pytest; **точка измерения — решение архитектора** |
|
||
| `src/main.py` | изменить | read-only блок `coverage` в `GET /queue` (наблюдаемость) |
|
||
| `docs/work-items/<id>/<NN>-coverage-report.md` | создать (артефакт run-time) | отчёт о покрытии с machine-readable вердиктом (см. §4/§6); номер/имя и регистрация в `docs/_standards/PIPELINE_DOCS.md` + скелет в `docs/_templates/` — оформляет архитектор |
|
||
| `tests/test_coverage_gate.py` | создать | unit/integration по `04-test-plan.yaml` |
|
||
|
||
## 3. Функциональные требования
|
||
|
||
### FR-1 — Измерение покрытия (привязка BR-1)
|
||
Гейт исполняет тест-сьют `orchestrator` (`python -m pytest tests/`, см. `.gitea/workflows/ci.yml`)
|
||
под coverage-инструментацией в изолированном per-branch worktree (`ensure_worktree`, прецедент
|
||
`check_tests_local`) и извлекает числовую метрику покрытия (как минимум суммарный line coverage,
|
||
`%`). Тайм-аут на прогон ограничен (по образцу `merge_retest_timeout_s` / `security_scan_timeout_s`).
|
||
|
||
### FR-2 — Решение гейта (привязка BR-2, BR-3)
|
||
Чистая функция `compute_coverage_verdict(measured, baseline, floor, policy, epsilon) -> (ok, reason)`:
|
||
- `policy = absolute` → PASS ⇔ `measured >= floor - epsilon`.
|
||
- `policy = baseline` → PASS ⇔ `measured >= baseline - epsilon`.
|
||
- `policy = both` (дефолт) → PASS ⇔ выполнены оба условия.
|
||
- FAIL → гейт инициирует штатный откат на `development` для доработки тестов (по образцу
|
||
`_handle_security_gate` / merge-gate rollback), с инкрементом счётчика developer-retry.
|
||
- `epsilon` — малый неотрицательный допуск на шум измерения (NFR-4), настраиваемый.
|
||
|
||
### FR-3 — Точка в конвейере (привязка BR-2; **кандидат, решает архитектор**)
|
||
Бизнес-запрос указывает «на testing-гейте». Грунтованные кодом кандидаты (выбрать один):
|
||
- **(a) Edge sub-gate** в `advance_stage` на ребре `deploy-staging → deploy` (рядом с
|
||
`_handle_security_gate`/`_handle_merge_gate`/`_handle_image_freshness`) — даёт гарантию «гейт
|
||
ДО слияния в `main`», детерминирован, владеет исходом на вмешательстве. Предпочтительно для
|
||
соответствия NFR-3/NFR-6.
|
||
- **(b) Под-гейт/расширение на ребре `testing → deploy-staging`** (рядом с `check_tests_passed`).
|
||
- **(c) CI-шаг** в `.gitea/workflows/ci.yml` (ребро `development → review`, читается
|
||
`check_ci_green`) — порог проверяется самим pytest-прогоном.
|
||
Требование, инвариантное к выбору: гейт обязан отработать **до фактического merge в `main`** и не
|
||
пропускать деградацию в `main`.
|
||
|
||
### FR-4 — Базовая линия и её обновление (привязка BR-3)
|
||
- Персистентное per-repo хранилище базовой линии покрытия `main` (БД-таблица ИЛИ файл в репо —
|
||
решает архитектор; при БД — аддитивная таблица, NFR-5).
|
||
- Bootstrap: первичная инициализация фактическим замером текущего `main`.
|
||
- Ratchet-up: при успешном слиянии задачи в `main` базовая линия обновляется значением
|
||
смёрженного покрытия, **только если оно ≥ текущей** (покрытие не откатывается вниз). Обновление
|
||
должно быть атомарным/сериализованным относительно параллельных слияний (опереться на окно
|
||
merge-lease, ORCH-043).
|
||
|
||
### FR-5 — Условность и kill-switch (привязка BR-4)
|
||
- `coverage_gate_enabled=False` → гейт инертен, конвейер 1:1 как до ORCH-027.
|
||
- `coverage_gate_repos` (CSV) — область применения; **пусто → только self-hosting**
|
||
(`is_self_hosting_repo`, по образцу `merge_gate`/`security_gate`/`image_freshness`).
|
||
- Вне области → no-op `(True, "Coverage gate N/A")` (прецедент `check_staging_status` для
|
||
не-self-hosting, ORCH-035).
|
||
- `applies(repo)` (локальная проверка) выполняется ПЕРВОЙ; дорогой прогон измерения — только при
|
||
`applies==True`.
|
||
|
||
### FR-6 — Поведение при ошибке инструмента (привязка NFR-2)
|
||
Ошибка/недоступность coverage-инструмента или невозможность распарсить метрику → по умолчанию
|
||
**fail-open + WARNING** (`coverage_tool_fail_closed=False`, прецедент `security_dep_audit_fail_closed`);
|
||
флаг переключает в fail-closed. Поведение логируется явной observability-строкой.
|
||
|
||
### FR-7 — Наблюдаемость (привязка BR-5)
|
||
- Артефакт-отчёт `<NN>-coverage-report.md` с machine-readable вердиктом (см. §4).
|
||
- Read-only блок `coverage` в `GET /queue` (per-repo: `enabled`/`policy`/`floor`/`baseline`/
|
||
последнее измеренное/вердикт).
|
||
- При FAIL — `send_telegram` (notifying) с кликабельным номером задачи (`plane_issue_link`),
|
||
измеренным покрытием, порогом/базовой линией и дельтой.
|
||
|
||
## 4. Изменения API
|
||
|
||
- **`GET /queue`** — добавить read-only блок `coverage` (наблюдаемость; форма прочих блоков
|
||
`serial_gate`/`security`/`merge`). Без изменения существующих полей ответа.
|
||
- **Опционально (решает архитектор):** ручной эндпоинт сброса/override базовой линии
|
||
(`POST /coverage/baseline?repo=…`) — по образцу `POST /serial-gate/unfreeze`, на случай
|
||
легитимного разового снижения покрытия. Если не вводится — override выполняется через конфиг.
|
||
- Существующие webhook-роуты (`/webhook/plane`, `/webhook/gitea`) — без изменений.
|
||
|
||
## 5. Изменения схемы БД
|
||
|
||
Зависит от выбора хранилища базовой линии (FR-4):
|
||
- **Если БД:** аддитивная таблица `coverage_baseline(repo TEXT PRIMARY KEY, coverage REAL,
|
||
updated_at, source_sha TEXT)` через `CREATE TABLE IF NOT EXISTS` (паттерн `repo_freeze`/
|
||
`job_deps`). Существующие таблицы — **не мигрируются** (NFR-5).
|
||
- **Если файл в репо:** изменений схемы БД нет (базовая линия — версионируемый файл вроде
|
||
`.coverage-baseline.json`, читаемый/обновляемый под merge-lease).
|
||
|
||
Выбор — архитектор; ТЗ требует лишь: персистентность, restart-safe, аддитивность, атомарность
|
||
обновления.
|
||
|
||
## 6. Требования к новым/изменённым QG checks
|
||
|
||
- **Новый машинный вердикт покрытия.** Если гейт реализован как edge sub-gate (FR-3a/b), он
|
||
**сам вычисляет** вердикт (как `check_security_gate`) и пишет отчёт `<NN>-coverage-report.md`
|
||
с frontmatter-ключом `coverage_status:` (`PASS` | `FAIL`), читаемым обратно из того же файла
|
||
(single source of truth, по образцу `security_status:` в `17-security-report.md`). Имя ключа
|
||
фиксируется и регистр чувствителен.
|
||
- **Реестр `QG_CHECKS`.** Допустимо добавить `check_coverage_gate` в реестр (если механизм —
|
||
зарегистрированный QG) ЛИБО оставить его врезкой-под-гейтом (как security/merge/image-freshness,
|
||
которые в `QG_CHECKS` присутствуют, но исполняются как врезки). **Семантика и состав
|
||
существующих `check_*` — без изменений** (NFR-5).
|
||
- **Парсинг frontmatter** вердикта — через единый контракт `src/frontmatter.py`
|
||
(`parse_frontmatter`/`read_frontmatter_value`), как все вердикт-парсеры (ORCH-052c). Если
|
||
отчёт несёт обязательную 6-польную схему 52c — добавить её аддитивно, не трогая `coverage_status:`.
|
||
|
||
## 7. Совместимость / регресс
|
||
|
||
- **Обратная совместимость:** при `coverage_gate_enabled=False` или для репозитория вне
|
||
`coverage_gate_repos` — поведение конвейера байт-в-байт прежнее; enduro-trails не затронут.
|
||
- **Kill-switch + поэтапный раскат:** `coverage_gate_enabled` (глобальный), `coverage_gate_repos`
|
||
(область). Старт — только `orchestrator`.
|
||
- **Конфиг-флаги (итог §3/§6):** `coverage_gate_enabled` (bool), `coverage_gate_repos` (CSV),
|
||
`coverage_min_percent` (float, абсолютный порог), `coverage_policy` (`absolute|baseline|both`,
|
||
дефолт `both`), `coverage_epsilon` (float, допуск шума), `coverage_tool_fail_closed` (bool,
|
||
дефолт `False`), `coverage_run_timeout_s` (int). Имена env — `ORCH_COVERAGE_*`.
|
||
- **never-raise / fail-open в hot-path:** ядро не роняет `advance_stage`; ошибка инструмента →
|
||
fail-open + warning по умолчанию (NFR-2). Прод-контейнер/`main`/force-push — не трогаются (NFR-3).
|
||
- **Restart-safe:** базовая линия персистентна; in-flight измерение при рестарте переигрывается
|
||
штатным механизмом стадии (idempotent).
|
||
- **Документация (golden source):** при выборе механизма архитектор регистрирует артефакт
|
||
`<NN>-coverage-report.md` и его machine-key в `docs/_standards/PIPELINE_DOCS.md` +
|
||
`docs/_templates/`, и обновляет `docs/architecture/README.md` и `CHANGELOG.md` в том же PR.
|