From 9953275eeda3729469c160e1ef7dc0ff223848d4 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 10 Jun 2026 00:32:28 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=532 --- docs/work-items/ORCH-027/01-brd.md | 166 ++++++++++++++++++ docs/work-items/ORCH-027/02-trz.md | 156 ++++++++++++++++ .../ORCH-027/03-acceptance-criteria.md | 138 +++++++++++++++ docs/work-items/ORCH-027/04-test-plan.yaml | 110 ++++++++++++ 4 files changed, 570 insertions(+) create mode 100644 docs/work-items/ORCH-027/01-brd.md create mode 100644 docs/work-items/ORCH-027/02-trz.md create mode 100644 docs/work-items/ORCH-027/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-027/04-test-plan.yaml diff --git a/docs/work-items/ORCH-027/01-brd.md b/docs/work-items/ORCH-027/01-brd.md new file mode 100644 index 0000000..2cdc0b8 --- /dev/null +++ b/docs/work-items/ORCH-027/01-brd.md @@ -0,0 +1,166 @@ +--- +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` (заполняет архитектор). diff --git a/docs/work-items/ORCH-027/02-trz.md b/docs/work-items/ORCH-027/02-trz.md new file mode 100644 index 0000000..0549c19 --- /dev/null +++ b/docs/work-items/ORCH-027/02-trz.md @@ -0,0 +1,156 @@ +--- +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//-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) +- Артефакт-отчёт `-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`) и пишет отчёт `-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):** при выборе механизма архитектор регистрирует артефакт + `-coverage-report.md` и его machine-key в `docs/_standards/PIPELINE_DOCS.md` + + `docs/_templates/`, и обновляет `docs/architecture/README.md` и `CHANGELOG.md` в том же PR. diff --git a/docs/work-items/ORCH-027/03-acceptance-criteria.md b/docs/work-items/ORCH-027/03-acceptance-criteria.md new file mode 100644 index 0000000..97c3d0b --- /dev/null +++ b/docs/work-items/ORCH-027/03-acceptance-criteria.md @@ -0,0 +1,138 @@ +--- +work_item: ORCH-027 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-027 — Code coverage как гейт + +Work Item: **ORCH-027** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** +(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам +репозитория. + +--- + +## AC-1 — Покрытие измеряется инструментально + +**Условие:** на применимом репозитории конвейер измеряет покрытие тестами исполнением сьюта под +coverage-инструментацией перед слиянием в `main`. +- **PASS:** в коде есть путь, который запускает `pytest` под coverage в изолированном worktree и + извлекает числовую метрику line coverage (`%`); coverage-зависимость добавлена в `requirements.txt`. +- **FAIL:** покрытие не измеряется инструментально, метрика берётся из прозы/вердикта LLM, либо + зависимость не объявлена. + +--- + +## AC-2 — Гейт блокирует деградацию + +**Условие:** покрытие ниже политики не пропускается дальше к деплою. +- **PASS:** при измеренном покрытии ниже порога/базовой линии (с учётом epsilon) гейт даёт FAIL и + инициирует штатный откат на `development` (инкремент developer-retry), задача не достигает `done`. +- **FAIL:** задача с упавшим покрытием проходит гейт и продвигается к деплою/`done`. + +--- + +## AC-3 — Чистая функция решения + +**Условие:** вердикт — детерминированная чистая функция от (measured, baseline, floor, policy, epsilon). +- **PASS:** `compute_coverage_verdict(...)` покрыта unit-тестами для всех режимов + (`absolute`/`baseline`/`both`), границ (равно порогу), epsilon-допуска; без участия LLM. +- **FAIL:** решение принимает LLM, либо логика недетерминирована/не покрыта тестами границ. + +--- + +## AC-4 — Режим базовой линии (ratchet) + +**Условие:** поддержан режим «не ниже предыдущего» с обновлением базовой линии вверх при слиянии. +- **PASS:** базовая линия персистентна per-repo; при слиянии обновляется значением смёрженного + покрытия только если оно ≥ текущей; bootstrap инициализирует её фактическим покрытием `main`; + обновление атомарно/сериализовано относительно параллельных слияний. +- **FAIL:** базовая линия не хранится / откатывается вниз / обновляется неатомарно (гонка двух + слияний теряет/занижает значение). + +--- + +## AC-5 — Условность и нулевая регрессия + +**Условие:** вне области / при выключенном флаге — поведение конвейера 1:1 как до ORCH-027. +- **PASS:** при `coverage_gate_enabled=False` или repo ∉ `coverage_gate_repos` гейт — no-op + (`(True, "...N/A")`); существующая тестовая база (`pytest tests/`) зелёная; enduro-trails не + затронут; `applies(repo)` проверяется до дорогого прогона. +- **FAIL:** гейт срабатывает вне области, либо выключенный флаг меняет поведение, либо есть + регресс существующих тестов. + +--- + +## AC-6 — Fail-open по умолчанию при ошибке инструмента + +**Условие:** сбой/недоступность coverage-инструмента не заклинивает автономный конвейер. +- **PASS:** при ошибке измерения и `coverage_tool_fail_closed=False` гейт даёт PASS + WARNING-лог + (observability-строка); флаг `=True` переключает в fail-closed (FAIL). Поведение покрыто тестом. +- **FAIL:** ошибка инструмента по умолчанию заворачивает задачу (петля rework) либо роняет + `advance_stage`. + +--- + +## AC-7 — never-raise / self-hosting безопасность + +**Условие:** ядро гейта не роняет конвейер и не трогает прод/`main`. +- **PASS:** `src/coverage_gate.py` — leaf (не импортирует `stage_engine`); любое исключение + перехвачено и не всплывает в `advance_stage`; код не вызывает деплой-хук, не перезапускает + прод-контейнер, не пушит/форс-пушит в `main`/`master`. +- **FAIL:** исключение из гейта всплывает в `advance_stage`; гейт трогает прод-контейнер или `main`. + +--- + +## AC-8 — Совместимость контрактов + +**Условие:** существующие машинные контракты не изменены. +- **PASS:** `STAGE_TRANSITIONS`, семантика существующих `check_*`, machine-verdict ключи + (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`) — байт-в-байт + прежние; любая новая БД-сущность аддитивна (без миграции существующих таблиц). +- **FAIL:** изменена семантика/имя существующего гейта или вердикт-ключа; миграция ломает + существующую схему. + +--- + +## AC-9 — Машинный вердикт покрытия и наблюдаемость + +**Условие:** результат измерения прозрачен и машинно читаем. +- **PASS:** при FAIL — Telegram-алерт с кликабельным номером задачи, измеренным покрытием, + порогом/базовой линией и дельтой; `GET /queue` несёт read-only блок `coverage`; артефакт-отчёт + с machine-readable вердиктом (`coverage_status: PASS|FAIL`) записан и читается обратно из того + же файла через `src/frontmatter.py`. +- **FAIL:** результат не виден в `GET /queue`/Telegram, либо вердикт парсится из прозы, а не из + frontmatter, либо имя ключа не зафиксировано (регистр). + +--- + +## AC-10 — Документация обновлена (golden source) + +**Условие:** документация синхронизирована с изменением в том же PR. +- **PASS:** если введён артефакт-отчёт — он зарегистрирован в `docs/_standards/PIPELINE_DOCS.md` + и `docs/_templates/`; обновлены `docs/architecture/README.md` (описание гейта/флагов) и + `CHANGELOG.md`; новые/изменённые инварианты несут маркер `ORCH-027`. +- **FAIL:** функционал введён без обновления обзорной/стандартной документации (reviewer → + REQUEST_CHANGES, ORCH-079). + +--- + +## Сводная матрица AC ↔ FR/BR + +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1 | +| AC-2 | BR-2 / FR-2, FR-3 | +| AC-3 | BR-2 / FR-2 / NFR-6 | +| AC-4 | BR-3 / FR-4 | +| AC-5 | BR-4 / FR-5 / NFR-5 | +| AC-6 | NFR-2 / FR-6 | +| AC-7 | NFR-1 / NFR-3 | +| AC-8 | NFR-5 / FR-6 (§6 ТЗ) | +| AC-9 | BR-5 / FR-7 / §6 ТЗ | +| AC-10 | Правила агентов §2/§6 (CLAUDE.md) | diff --git a/docs/work-items/ORCH-027/04-test-plan.yaml b/docs/work-items/ORCH-027/04-test-plan.yaml new file mode 100644 index 0000000..bbe6fe3 --- /dev/null +++ b/docs/work-items/ORCH-027/04-test-plan.yaml @@ -0,0 +1,110 @@ +work_item: ORCH-027 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +title: "Code coverage gate — защита от деградации покрытия тестами" +framework: pytest +scope: > + Покрываются: чистая логика вердикта покрытия (режимы absolute/baseline/both, границы, + epsilon), ratchet-обновление базовой линии, условность (kill-switch + per-repo область), + fail-open/fail-closed при ошибке инструмента, never-raise, наблюдаемость (GET /queue, + Telegram при FAIL), интеграция гейта в advance_stage / точку конвейера. Вне покрытия: + фактические измерители не-Python стеков (jest/jacoco), мутационное тестирование. +notes: > + Тесты не должны исполнять реальный прод-деплой и не трогают prod-контейнер/main. + Измерение покрытия в тестах мокается/стабится (фиктивная метрика), реальный pytest-прогон + под coverage проверяется отдельным интеграционным тестом на минимальном фикстур-репо/worktree. + Полный регресс tests/ должен оставаться зелёным (нулевая регрессия для enduro-trails). + +tests: + - id: TC-01 + type: unit + description: "compute_coverage_verdict, policy=absolute: measured>=floor → PASS; measured=baseline → PASS; ниже baseline-epsilon → FAIL (no-regression / ratchet)" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-03 + type: unit + description: "compute_coverage_verdict, policy=both: PASS только при выполнении обоих условий; нарушение любого → FAIL" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-04 + type: unit + description: "epsilon-допуск: дрожание покрытия в пределах epsilon у границы не заворачивает задачу (анти-флап, NFR-4)" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-05 + type: unit + description: "Ratchet базовой линии: при слиянии baseline растёт до смёрженного покрытия только если >= текущей; меньшее значение не понижает baseline" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-06 + type: unit + description: "Bootstrap базовой линии: первичная инициализация фактическим покрытием main при отсутствии сохранённого значения" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-07 + type: unit + description: "Условность applies(repo): пустой coverage_gate_repos → только self-hosting (is_self_hosting_repo); repo вне области → no-op (True, 'N/A'), дорогой прогон не запускается" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-08 + type: unit + description: "Kill-switch coverage_gate_enabled=False → гейт инертен, advance_stage ведёт себя 1:1 как до ORCH-027" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-09 + type: unit + description: "Fail-open по умолчанию: ошибка/недоступность coverage-инструмента и coverage_tool_fail_closed=False → PASS + WARNING-лог; флаг True → FAIL (fail-closed)" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-10 + type: unit + description: "never-raise: внутреннее исключение (битый вывод coverage, отсутствие worktree) перехватывается, не всплывает в advance_stage" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-11 + type: unit + description: "Запись/чтение отчёта: write_coverage_report пишет coverage_status: PASS|FAIL во frontmatter; parse читает обратно из того же файла через src/frontmatter.py (single source of truth)" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-12 + type: unit + description: "Self-hosting безопасность: гейт не вызывает деплой-хук, не перезапускает прод-контейнер, не пушит/форс-пушит в main/master" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-13 + type: integration + description: "Гейт в конвейере: при measured ниже политики advance_stage не продвигает к деплою и инициирует откат на development (инкремент developer-retry); при PASS — продвигает штатно" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-14 + type: integration + description: "Реальное измерение: pytest под coverage в ensure_worktree на минимальном фикстур-репо возвращает корректную метрику line coverage и тайм-аутится по coverage_run_timeout_s" + module: tests/test_coverage_gate.py + expected: PASS + + - id: TC-15 + type: integration + description: "Наблюдаемость: FAIL даёт Telegram-алерт с кликабельным номером (измеренное/порог/дельта); GET /queue несёт read-only блок coverage; совместимость — STAGE_TRANSITIONS/QG_CHECKS/существующие вердикт-ключи не изменены" + module: tests/test_coverage_gate.py + expected: PASS