From e9e8b1e246b18e62d12e05ba11f9520ec6da3782 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 10 Jun 2026 00:40:42 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=533 --- docs/_standards/PIPELINE_DOCS.md | 4 +- docs/_templates/18-coverage-report.md | 30 ++ docs/architecture/README.md | 43 ++- .../adr/adr-0029-coverage-gate.md | 93 ++++++ .../ORCH-027/06-adr/ADR-001-coverage-gate.md | 268 ++++++++++++++++++ .../ORCH-027/07-infra-requirements.md | 65 +++++ .../ORCH-027/08-data-requirements.md | 68 +++++ docs/work-items/ORCH-027/10-tech-risks.md | 43 +++ 8 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 docs/_templates/18-coverage-report.md create mode 100644 docs/architecture/adr/adr-0029-coverage-gate.md create mode 100644 docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md create mode 100644 docs/work-items/ORCH-027/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-027/08-data-requirements.md create mode 100644 docs/work-items/ORCH-027/10-tech-risks.md diff --git a/docs/_standards/PIPELINE_DOCS.md b/docs/_standards/PIPELINE_DOCS.md index ace257b..a17f07a 100644 --- a/docs/_standards/PIPELINE_DOCS.md +++ b/docs/_standards/PIPELINE_DOCS.md @@ -2,7 +2,7 @@ > **Назначение.** Единая карта «стадия → агент → документ → категория → гейт/механизм → > frontmatter machine-key» + конвенция ADR-naming. Это **golden source структуры** номерных -> документов work item (`00-business-request.md` … `17-security-report.md`), который каждая +> документов work item (`00-business-request.md` … `18-coverage-report.md`), который каждая > агентская роль пишет на своей стадии. > > **Статус истины (важно).** Манифест **документирует** текущее поведение гейтов, но НЕ является @@ -60,6 +60,7 @@ check_tests_passed → check_staging_status → check_deploy_status`. | `15-staging-log.md` | deployer | required (self-hosting) | `deploy-staging` | `check_staging_status` (self-hosting; иначе N/A — ORCH-35) | `staging_status:` (`SUCCESS` \| `FAILED`) | | `16-post-deploy-log.md` | post-deploy-monitor | when-applicable | пост-`done` наблюдение (ORCH-021; не ребро `STAGE_TRANSITIONS`) | информационный (гейтом не парсится) | `post_deploy_status:` (`HEALTHY` \| `DEGRADED`) | | `17-security-report.md` | security-гейт (детерминированный, ORCH-022) | when-applicable | под-гейт ребра `deploy-staging→deploy` | `check_security_gate` (врезка в `advance_stage`) | `security_status:` (`PASS` \| `FAIL`) | +| `18-coverage-report.md` | coverage-гейт (детерминированный, ORCH-027) | when-applicable | под-гейт ребра `deploy-staging→deploy` (ПОСЛЕ merge-gate, ДО image-freshness) | `check_coverage_gate` (врезка в `advance_stage`) | `coverage_status:` (`PASS` \| `FAIL`) | ### Примечания манифеста (нормативные) @@ -86,6 +87,7 @@ check_tests_passed → check_staging_status → check_deploy_status`. | `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` | `SUCCESS` → `done`; `FAILED` → откат (БАГ-8) | | `15-staging-log.md` | `staging_status:` | `_parse_staging_status` | `SUCCESS` → дальше; `FAILED` → откат (self-hosting; иначе N/A) | | `17-security-report.md` | `security_status:` | `check_security_gate` | `PASS` → дальше; `FAIL` → откат | +| `18-coverage-report.md` | `coverage_status:` | `check_coverage_gate` | `PASS` → дальше; `FAIL` → откат на `development` | **Информационные доки** — гейтом НЕ парсятся (структура ничего не блокирует): `00-business-request.md` (вход), `08-data-requirements.md`, `10-tech-risks.md`, diff --git a/docs/_templates/18-coverage-report.md b/docs/_templates/18-coverage-report.md new file mode 100644 index 0000000..e4b481d --- /dev/null +++ b/docs/_templates/18-coverage-report.md @@ -0,0 +1,30 @@ +--- +coverage_status: PASS # PASS | FAIL (machine-key — читает check_coverage_gate) +work_item: ORCH-NNN +measured_coverage: 0.0 # измеренное line coverage src/ (%, float) +baseline: 0.0 # базовая линия main на момент измерения (%, или пусто при bootstrap) +floor: 0.0 # абсолютный порог coverage_min_percent (%) +policy: both # absolute | baseline | both +epsilon: 0.5 # допуск на шум измерения (%) +delta: 0.0 # measured − max(baseline, floor) (%, знаковая дельта) +--- + +# Coverage Report — ORCH-NNN + +> Детерминированный гейт покрытия (ORCH-027) — под-гейт ребра `deploy-staging→deploy` (врезка в +> `advance_stage`, ПОСЛЕ merge-gate, ДО image-freshness; не строка `STAGE_TRANSITIONS`). Машинный +> вердикт читается ТОЛЬКО из `coverage_status:`. `PASS` → дальше; `FAIL` → откат на `development`. +> Измерение — `pytest --cov=src --cov-report=json` в изолированном worktree. Source of truth +> измеренного значения для ratchet базовой линии (`_handle_merge_verify`, ребро `deploy→done`). + +## Verdict + + +## Measurement +<Инструмент (pytest-cov/coverage.py), команда, line coverage src/ = X%; либо fail-open WARNING +при ошибке инструмента (coverage_tool_fail_closed=False).> + +## Policy +<Режим (absolute|baseline|both), порог floor, базовая линия main, epsilon, какое условие +нарушено при FAIL.> + diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 6a16160..ae4a622 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -39,7 +39,7 @@ created → analysis → architecture → development → review → testing → | deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) | | done | — | — | — | -**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022). +**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022), check_coverage_gate (ORCH-027). **Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. **Единый frontmatter-контракт (ORCH-52c / ORCH-076):** парсинг YAML-frontmatter сведён к одной точке — `src/frontmatter.parse_frontmatter` (структура `data/has_block/malformed/yaml_error`, never-raise); пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) делегируют ей вместо дублированной ad-hoc логики. Модуль также несёт writer (`render/write_frontmatter`), валидатор обязательной схемы (`validate_schema`/`REQUIRED_FIELDS`, warning-only по умолчанию; hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`) и общий `strip_frontmatter`. Семантика вердиктов / `STAGE_TRANSITIONS` / состав `QG_CHECKS` — без изменений (1:1). @@ -189,6 +189,47 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`. Безусловный pre-merge rebase + связь с зависимостями задач — [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md) (ORCH-026). +### Coverage-гейт: защита от деградации покрытия тестами (ORCH-027 — design) +Существующие тестовые гейты (`check_ci_green`, `check_tests_passed`, merge-gate re-test) судят +только по факту прохождения тестов, не по **полноте** — фича «300 строк, 0 тестов» проходит +незамеченной, и при пакетном автономном прогоне (ORCH-088) покрытие монотонно деградирует. +ORCH-027 вводит детерминированный (без LLM) **гейт покрытия как под-гейт ребра +`deploy-staging → deploy`** — рядом с security/merge/image-freshness, по тому же паттерну +(leaf `src/coverage_gate.py` never-raise + обёртка `check_coverage_gate` в `QG_CHECKS` + врезка +`_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` + (line coverage `src/`). Тайм-аут `coverage_run_timeout_s`. +- **Решение — чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy, + epsilon)`: `absolute` (≥floor−ε) / `baseline` (≥baseline−ε, ratchet) / `both` (дефолт); + `baseline=None` → bootstrap. 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). **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). +- **Условность (как ORCH-35/43/58):** `coverage_gate_enabled` + `coverage_gate_repos` (пусто → + только self-hosting `orchestrator`); вне области → no-op pass; `applies(repo)` ПЕРВОЙ. + **Ошибка инструмента → fail-open + WARNING** по умолчанию (`coverage_tool_fail_closed=False`, + анти-петля как ORCH-061); флаг → fail-closed. +- **Артефакт `18-coverage-report.md`** (frontmatter `coverage_status: PASS|FAIL` + + `measured_coverage`/`baseline`/`floor`/`policy`/`delta`), вердикт читается ТОЛЬКО из + frontmatter через `src/frontmatter.py`. Наблюдаемость — read-only блок `coverage` в + `GET /queue`; FAIL → Telegram (кликабельный номер, измеренное/порог/дельта); опциональный + `POST /coverage/baseline` (ручной override). never-raise; гейт не деплоит/не рестартит прод/ + не пушит в `main` (NFR-3). При выключенном флаге — нулевая регрессия (enduro не затронут). + +Подробнее: [adr-0029](adr/adr-0029-coverage-gate.md), детально — +`docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md`, +`docs/work-items/ORCH-027/08-data-requirements.md`. + ### Зависимости задач: B ждёт A (ORCH-026, Уровень B) Плоская очередь ORCH-1 (FIFO по `id` + `available_at` + `max_concurrency`) не выражала логических зависимостей. ORCH-026 вводит декларативные связи «задача B не стартует, пока не готовы её depends-on» — без новой стадии и без изменения `STAGE_TRANSITIONS`/`QG_CHECKS`. - **Источник истины планировщика — БД** (аддитивная таблица `job_deps(task_id, depends_on_task_id)`): claim в горячем цикле обслуживает очередь ВСЕХ проектов и обязан быть offline-устойчив (сетевой Plane на каждый claim = встанет очередь всех проектов). Источник **декларации** настраивается `task_deps_source = db|plane|hybrid` (дефолт `db`; `plane`/`hybrid` читают Plane relations в `handle_work_item_created` и кэшируют в `job_deps`). diff --git a/docs/architecture/adr/adr-0029-coverage-gate.md b/docs/architecture/adr/adr-0029-coverage-gate.md new file mode 100644 index 0000000..cf66ed5 --- /dev/null +++ b/docs/architecture/adr/adr-0029-coverage-gate.md @@ -0,0 +1,93 @@ +--- +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). + diff --git a/docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md b/docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md new file mode 100644 index 0000000..bdcce0d --- /dev/null +++ b/docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md @@ -0,0 +1,268 @@ +--- +work_item: ORCH-027 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# ADR-001: Гейт покрытия тестами — edge sub-gate с ratchet-базовой линией + +Work Item: **ORCH-027** — детерминированный гейт покрытия тестами, блокирующий деградацию +покрытия перед слиянием ветки задачи в `main`. +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0029-coverage-gate.md`** (решение +кросс-каттинговое — вводит новый QG `check_coverage_gate`, новый edge-под-гейт ребра +`deploy-staging→deploy`, новую аддитивную БД-таблицу `coverage_baseline` и новый артефакт +`18-coverage-report.md`). + +## Статус +Proposed + +--- + +## Контекст + +Оркестратор ведёт **автономную** разработку: код пишет агент `developer` без человека-фильтра, +а на стадии `testing` агент `tester` сам решает, достаточно ли тестов. Существующие тестовые +гейты судят только по **факту прохождения**, не по **полноте** (сверено по коду): + +- `check_ci_green` (`development → review`) — exit-code `pytest tests/` в Gitea CI + (`.gitea/workflows/ci.yml`); покрытие не меряется. +- `check_tests_passed` (`testing → deploy-staging`, `qg/checks.py::_parse_tests_verdict`) — + читает machine-verdict LLM-`tester`'а из `13-test-report.md`, а не измеренную метрику. +- Merge-gate re-test (ORCH-043, `src/merge_gate.py`) — повторный `pytest` на догнанной ветке, + снова только exit-code. + +Ни один гейт не замечает «300 строк кода, 0 тестов» или багфикс без регрессионного теста. При +пакетном автономном прогоне (ORCH-088, «10–20 задач за ночь») это означает **монотонную +деградацию покрытия**: каждая задача срезает угол на тестах, и за десятки задач проект тихо +теряет тестируемость. Нужна детерминированная метрика вместо доверия суждению агента — по духу +аналогично security-гейту (ORCH-022, adr-0012). + +Требования (`01-brd.md`/`02-trz.md`/`03-acceptance-criteria.md`): измерять покрытие +инструментально перед merge в `main` (BR-1/FR-1); блокировать деградацию относительно +абсолютного порога и/или базовой линии (BR-2/BR-3/FR-2); хранить и наращивать базовую линию +(ratchet, FR-4); kill-switch + per-repo область, нулевая регрессия для enduro-trails +(BR-4/FR-5); fail-open по умолчанию при сбое инструмента (NFR-2/FR-6); never-raise и +self-hosting-безопасность (NFR-1/NFR-3); неизменность существующих контрактов (NFR-5). + +## Решение + +### Сводка + +Вводим **детерминированный (без LLM) гейт покрытия** как **под-гейт ребра +`deploy-staging → deploy`** — рядом с security-gate (ORCH-022), merge-gate (ORCH-043) и +image-freshness (ORCH-058), исполняемый **ПОСЛЕ merge-gate и ДО image-freshness**. +`STAGE_TRANSITIONS` не меняется; в `QG_CHECKS` добавляется `check_coverage_gate`. Паттерн — +1:1 как у соседних под-гейтов: leaf-модуль `src/coverage_gate.py` (never-raise) + тонкая +обёртка в `QG_CHECKS` + врезка `_handle_coverage_gate` в `advance_stage`. Базовая линия `main` +хранится в **аддитивной БД-таблице** `coverage_baseline` и наращивается **вверх** (ratchet) в +choke-point подтверждённого merge `_handle_merge_verify` (ребро `deploy → done`). Вердикт +пишется в артефакт `18-coverage-report.md` (frontmatter-ключ `coverage_status:`) и читается +обратно из того же файла (single source of truth, как `security_status:`). + +### D1 — Точка в конвейере: edge sub-gate `deploy-staging → deploy`, ПОСЛЕ merge-gate (FR-3a) + +Из трёх кандидатов TRZ FR-3 выбран **(a) edge sub-gate** на ребре `deploy-staging → deploy` +(`advance_stage`, `src/stage_engine.py`, блок `current_stage == "deploy-staging"`). Это даёт +структурную гарантию «гейт ДО merge в `main`» (merge выполняется детерминированным merge-актором +в `_handle_merge_verify` на ребре `deploy → done`), детерминизм и владение исходом на +вмешательстве — полное соответствие NFR-3/NFR-6. + +**Порядок среди под-гейтов: security → merge → `coverage` → image-freshness.** Обоснование: + +- **ПОСЛЕ merge-gate (а не первым, как security).** Merge-gate выполняет догон ветки на свежий + `origin/main` (`auto_rebase_onto_main` под merge-lease, ORCH-043/026). Покрытие имеет смысл + мерить на **догнанном** HEAD — это ровно тот код, что landed в `main`; измерение до rebase + показало бы покрытие устаревшей базы. Поэтому coverage **обязан** идти после merge-gate + (в отличие от security, который специально фейлит дёшево ДО rebase). +- **ДО image-freshness.** Прогон pytest под coverage дорог, но дешевле полного docker-rebuild + staging-образа. Фейлить покрытие до rebuild — экономия (паттерн «fail before expensive + rebuild», 07-infra security-гейта). +- **Merge-lease held на этой точке.** Merge-gate уже захватил merge-lease (ORCH-043). Значит + **FAIL coverage обязан освободить merge-lease** при откате — как делает image-freshness + rollback (`merge_gate.release_merge_lease`, `stage_engine.py:1165`), и **в отличие** от + security-gate rollback (тот идёт ДО захвата lease и lease не трогает). Это явный инвариант + реализации (TR-2). + +Привязка: BR-2/FR-3/AC-2; NFR-3/AC-7. + +### D2 — Измеритель: `pytest-cov` (`coverage.py`), `--cov=src` (FR-1, BR-6) + +В `requirements.txt` добавляется **`pytest-cov`** (плагин-обёртка над `coverage.py`). Измерение — +прогон `python -m pytest tests/ --cov=src --cov-report=json:/coverage.json +--cov-report=` в изолированном per-branch worktree (`ensure_worktree`, прецедент +`check_tests_local`/merge-gate re-test). Числовая метрика — `totals.percent_covered` из JSON +(line coverage, `%`). Скоуп измерения — **`src/`** (не `tests/`: покрытие самих тестов вне +объёма, BRD §«Вне объёма»). Сеть при измерении не нужна. Тайм-аут — `coverage_run_timeout_s` +(по образцу `merge_retest_timeout_s`/`security_scan_timeout_s`). + +**Стек-расширяемость (BR-6/AC-… BR-6):** измеритель инкапсулирован за функцией +`measure_coverage(repo, branch) -> float | None`; чистая логика решения +`compute_coverage_verdict(...)` и хранилище базовой линии **не зависят** от Python/pytest. +Добавление jest/jacoco-измерителя для будущего стека — новая ветка `measure_*`, без переписывания +ядра. Фактическая интеграция не-Python стеков — вне объёма ORCH-027. + +### D3 — Чистая функция решения (FR-2, NFR-6, BR-2/BR-3) + +`compute_coverage_verdict(measured, baseline, floor, policy, epsilon) -> (ok: bool, reason: str)` — +детерминированная чистая функция (без LLM, без I/O): + +- `policy = "absolute"` → PASS ⇔ `measured >= floor - epsilon`. +- `policy = "baseline"` → PASS ⇔ `measured >= baseline - epsilon`. +- `policy = "both"` (дефолт) → PASS ⇔ выполнены **оба** условия. +- `baseline is None` (нет сохранённой базовой линии) → baseline-условие **не применяется** + (bootstrap: нельзя регрессировать против пустоты) → решает только absolute-часть; измеренное + значение засеет базовую линию при merge (D5). +- `epsilon` — малый неотрицательный допуск на шум измерения (NFR-4/AC-4): дрожание ±доли + процента у границы не заворачивает задачу. + +FAIL → штатный откат на `development` + инкремент общего `_developer_retry_count` (cap +`MAX_DEVELOPER_RETRIES`, затем `set_issue_blocked` + Telegram) — точно как security/merge-gate +rollback. Дословный reason (измеренное/порог/базовая линия/дельта) встраивается в `task_desc` +developer'а (паттерн ORCH-046). Привязка: AC-2/AC-3. + +### D4 — Хранилище базовой линии: аддитивная БД-таблица `coverage_baseline` (FR-4, NFR-5) + +Базовая линия `main` хранится в **БД**, не в файле репозитория: + +```sql +CREATE TABLE IF NOT EXISTS coverage_baseline ( + repo TEXT PRIMARY KEY, + coverage REAL NOT NULL, + source_sha TEXT, + updated_at TEXT NOT NULL +); +``` + +(паттерн `repo_freeze`/`job_deps` — `CREATE TABLE IF NOT EXISTS`, существующие таблицы не +мигрируются, NFR-5/AC-8; детали — `08-data-requirements.md`). **Почему БД, а не файл в репо** +(`.coverage-baseline.json`): файл пришлось бы коммитить в `main` на каждый ratchet → git-churn, +сам файл попадает в diff и может конфликтовать при параллельных merge, плюс он часть измеряемого +дерева. БД-таблица — restart-safe, аддитивна, обновляется атомарно и не порождает коммитов. +Таблица keyed by `repo` → общая прод-БД (self-hosting) безопасно разделяет базовые линии разных +репозиториев. + +### D5 — Ratchet-up в choke-point подтверждённого merge (FR-4, BR-3) + +Базовая линия наращивается **только вверх** и **только при подтверждённом** слиянии в `main`. +Единственный авторитетный choke-point подтверждённого merge — `_handle_merge_verify` (ребро +`deploy → done`, ORCH-071/073, доказательство SHA-in-main). Туда добавляется never-raise врезка +`coverage_gate.ratchet_baseline_on_merge(repo, work_item_id, branch, sha)`, вызываемая **после** +того как merge подтверждён (`_handle_merge_verify` вернул `False` = confirmed) и **до** перехода +в `done`: + +1. Читает измеренное покрытие смёрженной ветки из артефакта `18-coverage-report.md` (single + source of truth — то же значение, что гейт записал на ребре `deploy-staging→deploy`). +2. **Атомарный compare-and-set:** `UPDATE coverage_baseline SET coverage=?, source_sha=?, + updated_at=? WHERE repo=? AND coverage <= ?` (или `INSERT` при отсутствии строки — + bootstrap). Условие `coverage <= measured` гарантирует, что базовая линия **никогда не + падает** (FR-4), даже при гонке. + +**Сериализация (анти-гонка, NFR-5/AC-4):** на этой точке merge-lease ещё **held** (release на +`done`/rollback, `stage_engine.py:446`), а merge репо сериализован per-repo (ORCH-043). Плюс +атомарный compare-and-set в SQL — **двойная защита**: даже без lease два параллельных merge не +понизят и не потеряют значение. Bootstrap — первый merge применимого репо засевает базовую линию +своим измеренным покрытием. + +### D6 — Условность, kill-switch, наблюдаемость (FR-5/FR-7, BR-4/BR-5) + +- **Флаги (`config.py`, env `ORCH_COVERAGE_*`):** `coverage_gate_enabled` (bool, kill-switch), + `coverage_gate_repos` (CSV; **пусто → только self-hosting** `is_self_hosting_repo`, по образцу + `merge_gate`/`security_gate`/`image_freshness`), `coverage_min_percent` (float, абсолютный + порог-floor), `coverage_policy` (`absolute|baseline|both`, дефолт `both`), `coverage_epsilon` + (float, дефолт малый, напр. `0.5`), `coverage_tool_fail_closed` (bool, дефолт `False`), + `coverage_run_timeout_s` (int). +- **`applies(repo)`** (локальная проверка) выполняется **ПЕРВОЙ**; дорогой прогон измерения — + только при `applies==True`. Вне области → no-op `(True, "Coverage gate N/A")` (прецедент + `check_staging_status` для не-self, ORCH-035). При `coverage_gate_enabled=False` — гейт инертен, + конвейер 1:1 как до ORCH-027 (AC-5). +- **FR-6 (ошибка инструмента):** `measure_coverage` вернул `None` (инструмент упал/недоступен/ + метрика не распарсилась) → по умолчанию **fail-open + WARNING** (observability-строка), + `coverage_tool_fail_closed=True` → fail-closed (FAIL). Дефолт анти-петля (прецедент + ORCH-061/ORCH-022 dep-audit), чтобы инфра-сбой не заклинил автономный конвейер. +- **FR-7 (наблюдаемость):** артефакт `18-coverage-report.md` (frontmatter `coverage_status: + PASS|FAIL` + `measured_coverage`/`baseline`/`floor`/`policy`/`delta`); read-only блок + `coverage` в `GET /queue` (`src/main.py`); при FAIL — `send_telegram` с кликабельным номером + (`plane_issue_link`/`link_for`), измеренным покрытием, порогом/базовой линией и дельтой. + +### D7 — Машинный вердикт и парсинг (§6 ТЗ, AC-9) + +Гейт **сам вычисляет** вердикт (как `check_security_gate`) и пишет +`18-coverage-report.md` с YAML-frontmatter `coverage_status:` (`PASS` | `FAIL`); регистр +чувствителен, имя фиксируется. Чтение обратно — через единый контракт `src/frontmatter.py` +(`parse_frontmatter`/`read_frontmatter_value`, ORCH-052c), как все вердикт-парсеры. Артефакт +несёт **аддитивно** обязательную 6-польную схему 52c, не трогая `coverage_status:`. В `QG_CHECKS` +добавляется `check_coverage_gate` (тонкая обёртка, делегирующая в leaf); **семантика и состав +существующих `check_*` / machine-verdict ключей (`verdict:`/`result:`/`deploy_status:`/ +`staging_status:`/`security_status:`) — байт-в-байт прежние** (NFR-5/AC-8). + +### D8 — Опциональный override базовой линии (FR-4 / §4 API) + +Для легитимного разового снижения покрытия (напр. удаление большого протестированного модуля) +вводится опциональный ручной эндпоинт `POST /coverage/baseline?repo=&value=` (по +образцу `POST /serial-gate/unfreeze`) — устанавливает/сбрасывает базовую линию вручную. +Альтернатива без эндпоинта — временно переключить `coverage_policy=absolute`. Эндпоинт +рекомендован для эксплуатационной гибкости, но не критичен для v1. + +## Альтернативы + +- **Точка измерения — CI-job (`check_ci_green`, FR-3c).** Пороги/политика/базовая линия/артефакт + плохо выражаются статусом коммита; ratchet требует записи в общую БД, недоступную из CI-раннера + чисто. Коуплинг с раннером. Отклонено для v1 (точка расширения), как у security-гейта. +- **Точка измерения — `testing → deploy-staging` (рядом с `check_tests_passed`, FR-3b).** Ветка + ещё не догнана на свежий `main` → измеренное покрытие может не соответствовать landed-коду; + откат отсюда не освобождает merge-lease иначе. Edge `deploy-staging→deploy` после merge-gate — + точнее. Отклонено. +- **Базовая линия в файле репо (`.coverage-baseline.json`).** Git-churn на каждый ratchet, + конфликты при параллельных merge, файл — часть измеряемого дерева. Отклонено в пользу + аддитивной БД-таблицы (D4). +- **Складывание измерения в merge-gate re-test (один pytest-прогон).** Снижает дабл-ран, но + коуплит coverage-логику с merge_gate; нарушает leaf-изоляцию ТЗ. Отклонено для v1 (возможный + follow-up — измерять покрытие в том же прогоне). +- **Новый stage `coverage`.** «Пустая» стадия без агента не имеет триггера (как в ORCH-043/022). + Отклонено. +- **Жёсткий абсолютный порог без baseline/epsilon.** Массовые ложные заворота → петля rework. + Отклонено в пользу консервативного `both` + epsilon (NFR-4). + +## Последствия + +- **+** Класс «тихо просевшее покрытие» закрыт детерминированной метрикой; защита от монотонной + деградации в пакетном автономном прогоне (ORCH-088). Базовая линия может только расти (ratchet). +- **+** Нулевая регрессия: при выключенном флаге / вне области (enduro-trails) — конвейер + байт-в-байт прежний; `STAGE_TRANSITIONS`/`QG_CHECKS`-семантика/вердикт-ключи не тронуты. +- **+** Self-hosting-безопасно: гейт только мерит/читает/пишет/решает; не деплоит, не рестартит + прод, не пушит/форс-пушит в `main` (NFR-3). +- **−** Дополнительный прогон pytest под coverage на каждой применимой задаче (после merge-gate + re-test) → ещё один полный тест-ран. Митигейшн: ограничен `coverage_run_timeout_s`; фейлит до + дорогого image-rebuild; follow-up — слияние с merge-gate re-test. +- **−** Ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); новая pip-зависимость + (`pytest-cov`); v1 — Python-only (мульти-стек — точка расширения BR-6). +- **−** Дефолтный fail-open означает, что устойчивый сбой инструмента **тихо** пропускает задачи + (с WARNING). Митигейшн: громкий лог + переключатель `coverage_tool_fail_closed`. +- **Сквозное изменение** (новый QG + edge-под-гейт + новая БД-таблица + новый артефакт) → + лейбл `arch:major-change`; прод-деплой ORCH-027 — строго через staging-гейт (8501), без + рестарта прод-контейнера. +- **Откат:** `coverage_gate_enabled=False` → полный no-op (мгновенный обратимый kill-switch). + Полное удаление — снять врезки `_handle_coverage_gate`/`ratchet_baseline_on_merge`, удалить + leaf-модуль, `check_coverage_gate` из `QG_CHECKS`, флаги, артефакт-шаблон; таблица + `coverage_baseline` аддитивна и может остаться (инертна). + +## Ссылки + +- BRD: `docs/work-items/ORCH-027/01-brd.md` +- TRZ: `docs/work-items/ORCH-027/02-trz.md` +- Acceptance: `docs/work-items/ORCH-027/03-acceptance-criteria.md` +- Data: `docs/work-items/ORCH-027/08-data-requirements.md` +- Risks: `docs/work-items/ORCH-027/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0029-coverage-gate.md` +- Сверено по коду: `src/stage_engine.py` (`_handle_security_gate`/`_handle_merge_gate`/ + `_handle_image_freshness`/`_handle_merge_verify`), `src/security_gate.py`, `src/merge_gate.py`, + `src/qg/checks.py`, `.gitea/workflows/ci.yml`, `pytest.ini` +- Прецеденты: adr-0012 (security-гейт), adr-0006 (merge-gate — edge-под-гейт/откат/lease), + adr-0008 (image-freshness — условность/fail-closed), adr-0003 (`is_self_hosting_repo`), + adr-0009 (анти-петля ложных FAIL), adr-0014 (SHA-in-main как source of truth для merge) + + diff --git a/docs/work-items/ORCH-027/07-infra-requirements.md b/docs/work-items/ORCH-027/07-infra-requirements.md new file mode 100644 index 0000000..b9d9464 --- /dev/null +++ b/docs/work-items/ORCH-027/07-infra-requirements.md @@ -0,0 +1,65 @@ +--- +work_item: ORCH-027 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 07 — Инфраструктурные требования: ORCH-027 — Code coverage как гейт + +Work Item: **ORCH-027** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable. Топология **не меняется** (всё в существующем Docker-контейнере на одном +> сервере mva154, SQLite, собственная очередь). Затрагивается только зависимостный и +> конфигурационный слой. + +## Топология / окружение + +- **Без изменений топологии** — никаких новых контейнеров/сервисов/нод. Гейт исполняется внутри + существующего процесса оркестратора, измерение — в per-branch worktree (`ensure_worktree`), + как merge-gate re-test. `docs/operations/INFRA.md` — без правок. +- **Self-hosting безопасность (NFR-3):** гейт не вызывает деплой-хук, не рестартит прод-контейнер + `orchestrator` (8500), не пушит в `main`. Прод-деплой ORCH-027 — **только** через + staging-гейт (8501) → выделенный статус «Confirm Deploy» (ORCH-059), без рестарта прод + случайным approve. + +## Зависимости + +| Зависимость | Где | Назначение | +|-------------|-----|-----------| +| `pytest-cov` (обёртка `coverage.py`) | `requirements.txt` | измерение line coverage прогоном `pytest --cov=src --cov-report=json`. Offline (сеть при измерении не нужна). Попадает в прод-образ при пересборке. | + +- Версия фиксируется совместимой с текущим `pytest` (см. `requirements.txt`/`pytest.ini`). +- Новых системных пакетов в `Dockerfile` не требуется (чистый pip-пакет). + +## Конфигурация (env, `.env` на хосте) + +Новые флаги (`config.py`, префикс `ORCH_COVERAGE_*`; дефолты безопасны — нулевая регрессия): + +| Env | Дефолт | Назначение | +|-----|--------|-----------| +| `ORCH_COVERAGE_GATE_ENABLED` | `false` (раскат поэтапный) | kill-switch | +| `ORCH_COVERAGE_GATE_REPOS` | пусто → только self-hosting | CSV область применения | +| `ORCH_COVERAGE_MIN_PERCENT` | консервативно (напр. backstop) | абсолютный порог-floor | +| `ORCH_COVERAGE_POLICY` | `both` | `absolute\|baseline\|both` | +| `ORCH_COVERAGE_EPSILON` | малый (напр. `0.5`) | допуск на шум измерения | +| `ORCH_COVERAGE_TOOL_FAIL_CLOSED` | `false` | поведение при сбое инструмента | +| `ORCH_COVERAGE_RUN_TIMEOUT_S` | по образцу `merge_retest_timeout_s` | тайм-аут прогона | + +## Эксплуатационные предусловия + +- **Bootstrap базовой линии:** при первом merge применимого репо базовая линия `main` + засевается автоматически фактическим измеренным покрытием (D5). Ручной первичный замер не + обязателен; при необходимости — `POST /coverage/baseline?repo=orchestrator&value=<%>` (D8). +- **Раскат:** включать `ORCH_COVERAGE_GATE_ENABLED=true` только после прод-деплоя кода и + прогона на staging (8501); стартовая область — только `orchestrator`. +- **Override (легитимное снижение покрытия):** `POST /coverage/baseline` (по образцу + `POST /serial-gate/unfreeze`) либо временный `ORCH_COVERAGE_POLICY=absolute`. + +## Секреты / сеть + +- Новых секретов нет. Сетевого доступа при измерении нет (coverage offline). +- enduro-trails и прочие репозитории — вне области по умолчанию, нулевое влияние. + diff --git a/docs/work-items/ORCH-027/08-data-requirements.md b/docs/work-items/ORCH-027/08-data-requirements.md new file mode 100644 index 0000000..74a735c --- /dev/null +++ b/docs/work-items/ORCH-027/08-data-requirements.md @@ -0,0 +1,68 @@ +--- +work_item: ORCH-027 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 08 — Требования к данным: ORCH-027 — Code coverage как гейт + +Work Item: **ORCH-027** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable / информационный (гейтом не парсится). Затрагивается схема БД — вводится +> **одна аддитивная таблица** базовой линии покрытия. Существующие таблицы не мигрируются. + +## Изменения схемы БД + +Новая аддитивная таблица `coverage_baseline` (паттерн `repo_freeze`/`job_deps` — +`CREATE TABLE IF NOT EXISTS` в `init_db`, `src/db.py`; без `ALTER`/миграции существующих): + +```sql +CREATE TABLE IF NOT EXISTS coverage_baseline ( + repo TEXT PRIMARY KEY, -- репозиторий (напр. "orchestrator") + coverage REAL NOT NULL, -- last-known базовая линия покрытия main (%, line coverage) + source_sha TEXT, -- SHA main, на котором зафиксирована базовая линия (аудит) + updated_at TEXT NOT NULL -- ISO-таймстамп последнего ratchet/bootstrap +); +``` + +Доступ — через аддитивные read-only/мутирующие хелперы `src/db.py`: +- `get_coverage_baseline(repo) -> float | None` (None ⇒ bootstrap-режим, базовой линии ещё нет); +- `ratchet_coverage_baseline(repo, coverage, sha) -> bool` — **атомарный compare-and-set**: + `INSERT` при отсутствии строки; иначе `UPDATE ... SET coverage=?, source_sha=?, updated_at=? + WHERE repo=? AND coverage <= ?` (базовая линия **никогда не понижается**); +- `set_coverage_baseline(repo, coverage, sha)` — безусловная установка (ручной override D8 / + `POST /coverage/baseline`). + +## Новые/изменённые сущности + +- **`coverage_baseline`** — одна строка на репозиторий; keyed by `repo`. Инвариант: `coverage` + монотонно не убывает через `ratchet_coverage_baseline` (только `set_coverage_baseline`/ручной + override может понизить — легитимный разовый случай, D8). На общей прод-БД (self-hosting) + строки разных репозиториев изолированы первичным ключом. +- **Артефакт `18-coverage-report.md`** — НЕ БД-сущность: файл в `docs/work-items//`, + несёт frontmatter `coverage_status: PASS|FAIL` + `measured_coverage`/`baseline`/`floor`/ + `policy`/`delta`. Source of truth измеренного значения для ratchet (D5). + +Существующие таблицы (`tasks`, `jobs`, `job_deps`, `repo_freeze`, `agent_runs`, +`tracker_messages`, …) — **не изменяются** (NFR-5/AC-8). + +## Совместимость данных / миграции + +- **Аддитивность:** только `CREATE TABLE IF NOT EXISTS` — ни один существующий столбец/таблица + не трогается; миграции существующих данных нет. +- **Идемпотентность:** `CREATE TABLE IF NOT EXISTS` безопасен при повторном старте; bootstrap + (первый `INSERT`) выполняется один раз на репозиторий. +- **Restart-safe:** базовая линия персистентна; in-flight измерение при рестарте переигрывается + штатным механизмом стадии (idempotent — гейт пересчитает вердикт, ratchet — атомарный + compare-and-set, повтор не понизит и не задвоит). +- **Атомарность / анти-гонка:** ratchet — единичный SQL `UPDATE ... WHERE coverage <= ?` (или + `INSERT`), выполняется под held merge-lease (ORCH-043, per-repo сериализация merge) → двойная + защита от параллельных слияний. +- **Влияние на общую прод-БД:** одна маленькая таблица (≤ числа репозиториев строк); нулевой + риск для enduro-trails и прочих проектов (строки изолированы по `repo`, гейт для них no-op). +- При `coverage_gate_enabled=False` таблица может существовать пустой/инертной — нулевая + регрессия. + diff --git a/docs/work-items/ORCH-027/10-tech-risks.md b/docs/work-items/ORCH-027/10-tech-risks.md new file mode 100644 index 0000000..804019b --- /dev/null +++ b/docs/work-items/ORCH-027/10-tech-risks.md @@ -0,0 +1,43 @@ +--- +work_item: ORCH-027 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-027 — Code coverage как гейт + +Work Item: **ORCH-027** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | **Флап на шуме измерения** — недетерминированное покрытие (порядок тестов/окружение) дрожит у границы → ложные заворота, петля rework. | Сред. | Сред. | `coverage_epsilon` (NFR-4/D3): дрожание ±доли % не заворачивает. Дефолт `policy=both` мягкий; абсолютный порог — backstop, не агрессивный. | +| TR-2 | **Не освобождён merge-lease при FAIL.** Coverage идёт ПОСЛЕ merge-gate (lease уже held) — забытый release при откате заклинит serial-gate репо (другие задачи репо в defer навсегда). | Сред. | Выс. | Явный инвариант D1: rollback coverage вызывает `merge_gate.release_merge_lease` (как image-freshness rollback, `stage_engine.py:1165`); покрыто тестом TC-13. Backstop — crash-реклейм lease по возрасту (ORCH-043). | +| TR-3 | **Гонка базовой линии** — два параллельных слияния в `main` конкурентно обновляют baseline, теряя/занижая значение. | Низ. | Сред. | Атомарный SQL compare-and-set `UPDATE ... WHERE coverage <= ?` (D5/08-data) + held merge-lease + per-repo сериализация merge (ORCH-043) → тройная защита. Покрыто TC-05. | +| TR-4 | **Инфра-хрупкость инструмента** — `pytest-cov` несовместим с версией pytest / упал / метрика не парсится → конвейер клинит. | Низ. | Сред. | NFR-2/FR-6/D6: дефолт fail-open + громкий WARNING (анти-петля ORCH-061); `coverage_tool_fail_closed` для строгого режима. `measure_coverage`→`None` обрабатывается, не всплывает. Покрыто TC-09. | +| TR-5 | **Исключение всплывает в `advance_stage`** — ошибка leaf-модуля роняет конвейер ВСЕХ проектов (общий прод-инстанс). | Низ. | Выс. | NFR-1/AC-7: `src/coverage_gate.py` — leaf (не импортирует `stage_engine`), контракт never-raise; любое исключение → `(False/True, reason)` по политике fail-open/closed. Покрыто TC-10. | +| TR-6 | **Дабл-ран pytest** — coverage-прогон после merge-gate re-test удваивает время тестов на применимой задаче. | Выс. | Низ. | Ограничен `coverage_run_timeout_s`; фейлит ДО дорогого image-rebuild; follow-up — слияние измерения с merge-gate re-test (вне объёма v1). Влияет только на self-hosting `orchestrator`. | +| TR-7 | **Стартовая петля заворотов** — высокий `coverage_min_percent` массово заворачивает существующие задачи в rework. | Сред. | Сред. | NFR-4/D3: bootstrap инициализирует baseline фактическим покрытием `main`; absolute-порог — мягкий backstop; cap `MAX_DEVELOPER_RETRIES` → Blocked+alert вместо бесконечной петли. | +| TR-8 | **Self-hosting побочка** — гейт случайно трогает прод-контейнер/`main`/force-push. | Низ. | Выс. | NFR-3/AC-7: гейт только мерит/читает/пишет/решает в изолированном worktree; не вызывает деплой-хук, не рестартит прод, не пушит в `main`. Покрыто TC-12. | +| TR-9 | **Регресс контрактов** — затронуты `STAGE_TRANSITIONS`/существующие `check_*`/вердикт-ключи. | Низ. | Выс. | NFR-5/AC-8: новый QG аддитивен, edge-врезка не меняет `STAGE_TRANSITIONS`; вердикт-ключи прежних доков байт-в-байт. Покрыто TC-15. | + +## Сводный вывод + +Доминирующий класс рисков — **эксплуатация автономного self-hosting-конвейера**: самые +тяжёлые по влиянию (TR-2 заклинивание serial-gate, TR-5 падение конвейера всех проектов, TR-8 +побочка на прод) имеют **низкую вероятность** и закрыты структурными инвариантами, повторяющими +проверенные паттерны соседних под-гейтов (security/merge/image-freshness): leaf never-raise, +fail-open дефолт, явный release merge-lease при откате, kill-switch. Остаточный риск для +прод-конвейера — **низкий** при условии тестового покрытия инвариантов TR-2/TR-5/TR-8 +(`04-test-plan.yaml` TC-09…TC-13) и поэтапного раската через staging-гейт (8501). + +Решение **сквозное** (новый QG + edge-под-гейт + новая БД-таблица + новый артефакт) → эскалация +лейблом **`arch:major-change`**. Возврат в анализ не требуется — ТЗ реализуемо без нарушения +принципов архитектуры (Docker/один сервер/SQLite/собственная очередь сохранены). +