architect(ET): auto-commit from architect run_id=533

This commit is contained in:
2026-06-10 00:40:42 +03:00
committed by orchestrator-deployer
parent 9953275eed
commit e9e8b1e246
8 changed files with 612 additions and 2 deletions

View File

@@ -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`,

30
docs/_templates/18-coverage-report.md vendored Normal file
View File

@@ -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
<PASS / FAIL: measured X% vs floor F% / baseline B% (policy=…, epsilon=…), delta=±D%.>
## 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.>
</content>

View File

@@ -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`).

View File

@@ -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).
</content>

View File

@@ -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, «1020 задач за ночь») это означает **монотонную
деградацию покрытия**: каждая задача срезает угол на тестах, и за десятки задач проект тихо
теряет тестируемость. Нужна детерминированная метрика вместо доверия суждению агента — по духу
аналогично 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:<tmp>/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=<repo>&value=<float>` (по
образцу `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)
</content>
</invoke>

View File

@@ -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 и прочие репозитории — вне области по умолчанию, нулевое влияние.
</content>

View File

@@ -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/<id>/`,
несёт 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` таблица может существовать пустой/инертной — нулевая
регрессия.
</content>

View File

@@ -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/собственная очередь сохранены).
</content>