analyst(ET): auto-commit from analyst run_id=435
This commit is contained in:
145
docs/work-items/ORCH-088/01-brd.md
Normal file
145
docs/work-items/ORCH-088/01-brd.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 01 — BRD: ORCH-088 — Пакетный автономный режим (Этап 1: serial e2e)
|
||||
|
||||
Work Item: **ORCH-088**
|
||||
Repo: **orchestrator** (self-hosting)
|
||||
Стадия: analysis
|
||||
Заказчик: Слава
|
||||
Тип: ЭПИК — Этап 1 (минимальный, без параллелизма)
|
||||
|
||||
> ⚠️ **Скоп зафиксирован Владельцем 09.06.** Реализуется ТОЛЬКО serial e2e (FR-1…FR-5).
|
||||
> Фазовый режим A/B/C, merge-очередь FIFO, pre-merge rebase и зависимость от ORCH-83 —
|
||||
> **ОТМЕНЕНЫ, не реализовывать.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
### 1.1. Цель эпика
|
||||
Дать оркестратору **масштаб автономности**: накидать вечером 10–20 задач и получить к утру
|
||||
последовательно проведённый через весь конвейер (analysis → … → deploy → done) пакет — без
|
||||
ручного запуска каждой задачи и без взаимного повреждения веток.
|
||||
|
||||
### 1.2. Корневая проблема — «stale-анализ» (логический, а не код-затирание)
|
||||
Конвейер создаёт ветку задачи от `main`. Если задача **N+1** входит в анализ, пока задача **N**
|
||||
ещё **не влита в `main`**, то ветка N+1 срезается от **устаревшего** `main` (без кода N). Результат:
|
||||
- семантически устаревшая база разработки;
|
||||
- риск потери/переоткрытия уже сделанного в N (накопительные потери прецедента — постмортем
|
||||
фантомного merge, см. CLAUDE.md / ORCH-071);
|
||||
- ручной разбор конфликтов утром вместо готового пакета.
|
||||
|
||||
Физическое **код-затирание** при параллельном merge уже закрыто (ORCH-026 auto_rebase + merge-lease).
|
||||
ORCH-088 закрывает **логический** разрыв: гарантирует, что каждая следующая задача стартует от
|
||||
`main`, **уже содержащего все предыдущие завершённые задачи репо**.
|
||||
|
||||
### 1.3. Почему сериализация именно «от АНАЛИЗА», а не «от merge»
|
||||
Ветка срезается в самом начале — на входе в анализ (`start_pipeline` создаёт ветку в Gitea, далее
|
||||
worktree). Если допустить параллельный анализ N и N+1, ветка N+1 уже срезана от старого `main` —
|
||||
поздняя сериализация на merge проблему не лечит. Поэтому gate ставится на **входе новой задачи в
|
||||
анализ**: новая задача не начинает анализ (и не режет ветку), пока в репо есть незавершённая задача.
|
||||
|
||||
### 1.4. Установленные факты (проверено, не изобретать)
|
||||
- **Plane API v1:** bulk-операций НЕТ; issue-relation НЕТ → зависимости/очередь оркестратор хранит
|
||||
**у себя** (gate в планировщике/claim по локальной БД), не в Plane.
|
||||
- **Уже есть (переиспользовать):** `max_concurrency=1`; ORCH-026 auto_rebase_onto_main +
|
||||
force-with-lease + merge-lease; персистентная очередь ORCH-1 (таблица `jobs`, atomic claim,
|
||||
restart-safe); ORCH-021 post-deploy monitor (для self — всегда `ALERT_ONLY`, db-стадия `done`
|
||||
достигается ДО окна мониторинга — ORCH-071/066).
|
||||
|
||||
### 1.5. Решения Владельца (09.06) — приняты как требования
|
||||
| # | Решение |
|
||||
|---|---------|
|
||||
| D-1 | Serial e2e подтверждён. BRD появляются **по одному** — осознанный размен: надёжность > батч-просмотр BRD. |
|
||||
| D-2 | Сигнал «задача завершена» = **успешный прод-деплой** (`stage = done` после прод-деплоя). НЕ merge, НЕ staging. |
|
||||
| D-3 | Мониторинг (~15 мин) **НЕ ждём**: gate N+1 открывается по `stage = done`, не по завершению окна мониторинга. |
|
||||
| D-4 | Auto-rollback прода во время мониторинга → **заморозить gate + алерт**; следующая НЕ стартует до ручного снятия. |
|
||||
| D-5 | Зависимость ORCH-088 ← ORCH-83 **убрана** — запускается независимо. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### 2.1. В объёме (Этап 1)
|
||||
- **FR-1 — Serial gate (per-repo):** новая задача не входит в `analysis` (не режет ветку, не
|
||||
запускает analyst), пока в том же репо есть незавершённая задача (`stage < done`).
|
||||
- **FR-2 — Очередь e2e:** накиданные задачи становятся в очередь и обрабатываются **строго по одной**
|
||||
end-to-end (от анализа до прод-деплоя).
|
||||
- **FR-3 — Per-repo изоляция:** сериализация действует **внутри одного репо**; разные репо
|
||||
(`orchestrator`, `enduro-trails`) идут **параллельно** (независимые `main`).
|
||||
- **FR-4 — Restart-safe:** активная задача и состояние gate определяются по **БД** (не in-memory) —
|
||||
переживают рестарт оркестратора.
|
||||
- **FR-5 — Rollback-freeze:** auto-rollback / деградация прода → gate репо **заморожен** + Telegram-
|
||||
алерт; следующая задача не стартует до **ручного** снятия заморозки.
|
||||
|
||||
### 2.2. Вне объёма (явно, не делать)
|
||||
- Merge-очередь FIFO; pre-merge rebase как отдельная фича; фазовый режим A/B/C; любая координация
|
||||
**параллелизма** задач внутри одного репо.
|
||||
- Изменение `STAGE_TRANSITIONS`, реестра `QG_CHECKS`, новых стадий конвейера.
|
||||
- Зависимость от ORCH-83.
|
||||
|
||||
---
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
- **Владелец/оператор (Слава):** накидывает пакет вечером, разбирает заморозку при сбое, читает
|
||||
алерты, снимает freeze вручную.
|
||||
- **Self-hosting прод (`orchestrator`):** обслуживает enduro-trails из того же инстанса — нельзя
|
||||
ронять/блокировать конвейер enduro (FR-3).
|
||||
|
||||
---
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
|
||||
| ID | Требование | Связь |
|
||||
|----|------------|-------|
|
||||
| BR-1 | Пока в репо есть задача со `stage < done`, любая **другая** задача того же репо не начинает анализ — ждёт в очереди. | FR-1, AC-1 |
|
||||
| BR-2 | Как только активная задача достигла `stage = done` (после прод-деплоя), следующая задача того же репо **автоматически** стартует анализ. | FR-1/FR-2, AC-2, D-2 |
|
||||
| BR-3 | Ветка новой задачи срезается от `main`, **уже содержащего все ранее завершённые задачи репо** — нет stale-base. Branch не создаётся раньше, чем предшественник завершён. | FR-1, AC-6, §1.2 |
|
||||
| BR-4 | Сериализация — строго per-repo; задачи разных репо идут параллельно, gate одного репо не влияет на другой. | FR-3, AC-4 |
|
||||
| BR-5 | Активная задача и факт заморозки определяются из БД; после рестарта оркестратора gate ведёт себя идентично (не «забывает» активную задачу и не «теряет» freeze). | FR-4, AC-3 |
|
||||
| BR-6 | Auto-rollback/деградация прода (post-deploy) → per-repo freeze + Telegram-алерт; следующая задача не стартует до ручного снятия freeze. | FR-5, AC-5, D-4 |
|
||||
| BR-7 | Мониторинг прода (~15 мин) gate **не ждёт** — открытие gate привязано к `stage = done`. (Freeze BR-6 — отдельный, независимый от `stage` сигнал, т.к. к моменту деградации задача уже `done`.) | D-3, AC-5 |
|
||||
| BR-8 | Поведение управляется kill-switch'ом и областью репо (как ORCH-35/43/58): выключение флага → строго прежнее поведение (нулевая регрессия для enduro). | NFR |
|
||||
| BR-9 | Состояние gate наблюдаемо в `GET /queue` (активная задача репо, очередь ожидающих, статус freeze). | NFR |
|
||||
|
||||
---
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
|
||||
| ID | Требование |
|
||||
|----|------------|
|
||||
| NFR-1 | **never-raise:** любая ошибка логики gate не роняет claim/конвейер. Поведение при ошибке БД — **fail-open** для claim (транзиентный сбой не должен заклинить очередь ВСЕХ проектов), **fail-closed** для freeze (сомнение в безопасности прода → не стартовать). |
|
||||
| NFR-2 | **Offline-устойчивость:** проверка gate в горячем цикле claim не должна ходить в сеть (Plane/Gitea) — иначе встанет очередь всех проектов. Источник истины — локальная БД. |
|
||||
| NFR-3 | **Restart-safe:** никакого in-memory состояния; freeze и активная задача — в БД. |
|
||||
| NFR-4 | **Нулевая регрессия:** при выключенном флаге запрос claim и путь старта идентичны текущим; enduro не затрагивается. |
|
||||
| NFR-5 | **Инварианты неизменны:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды deploy-хука, merge-gate, схема post-deploy — не меняются (допустима только аддитивная, идемпотентная миграция БД). |
|
||||
| NFR-6 | **Self-hosting безопасность:** механизм не рестартит/не роняет прод-контейнер; freeze — пассивная остановка стартов, не действие над прод. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- `max_concurrency = 1` остаётся (Этап 1 без параллелизма); gate не зависит от значения, но не
|
||||
ослабляет его.
|
||||
- «Завершена» = `tasks.stage = 'done'`. Для self-hosting `done` достигается merge-verify + прод-деплой
|
||||
(ORCH-071/036); пост-деплойное окно мониторинга идёт **после** `done` и gate его не ждёт (BR-7).
|
||||
- Задача в статусе **Blocked / Needs Input** имеет `stage < done` и, следовательно, **держит gate
|
||||
закрытым** — это сознательное поведение (Этап 1): пока задача не доведена до прод или не закрыта
|
||||
оператором, пакет не движется. (Поведение зафиксировать в AC; альтернатива — вне скопа.)
|
||||
- Снятие freeze (BR-6) — **ручное** (оператор), автоматического разбора деградации нет.
|
||||
|
||||
---
|
||||
|
||||
## 7. Критерии успеха (резюме; детали — 03-acceptance-criteria.md)
|
||||
- AC-1 активная задача (`stage<done`) → новая не стартует анализ.
|
||||
- AC-2 активная достигла `done` → следующая стартует автоматически.
|
||||
- AC-3 gate переживает рестарт (состояние в БД).
|
||||
- AC-4 разные репо идут параллельно.
|
||||
- AC-5 auto-rollback → freeze + алерт, следующая не стартует до ручного снятия.
|
||||
- AC-6 каждая ветка срезана от `main` со всеми предыдущими завершёнными задачами репо (нет stale-base).
|
||||
|
||||
---
|
||||
|
||||
## 8. Риски (детали — 10-tech-risks.md, заполняет архитектор)
|
||||
- R-1: stale-base сохраняется, если ветка режется на входе (`_create_gitea_branch` в `start_pipeline`)
|
||||
до завершения предшественника — gate обязан отсрочить **создание ветки**, а не только claim.
|
||||
- R-2: gate, ошибочно fail-closed на транзиентной ошибке БД, заклинит очередь всех проектов.
|
||||
- R-3: «вечный freeze» / залипшая активная задача в Blocked останавливает пакет — нужна наблюдаемость
|
||||
и ручное снятие.
|
||||
210
docs/work-items/ORCH-088/02-trz.md
Normal file
210
docs/work-items/ORCH-088/02-trz.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 02 — ТЗ (TRZ): ORCH-088 — Serial gate (Этап 1: пакетный автономный режим, serial e2e)
|
||||
|
||||
Work Item: **ORCH-088** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> Документ описывает **что** должно измениться и **где** (модули/контракты/артефакты). **Как**
|
||||
> (конкретная схема реализации, выбор «таблица vs sentinel», точки врезки) — решает архитектор в
|
||||
> `06-adr/`. ТЗ фиксирует требования и границы, не предлагает архитектурное решение.
|
||||
|
||||
> ⚠️ Скоп — только FR-1…FR-5 (serial e2e). Merge-очередь / pre-merge rebase / фазы A/B/C / ORCH-83 —
|
||||
> вне скопа.
|
||||
|
||||
---
|
||||
|
||||
## 1. Сводка изменения
|
||||
Ввести **per-repo serial gate**: новая задача репо не входит в стадию `analysis` (не режет ветку, не
|
||||
запускает analyst-агент), пока в том же репо есть незавершённая задача (`stage != 'done'`). Открытие
|
||||
gate — по достижении предшественником `stage = 'done'` (после прод-деплоя). Дополнительно — **per-repo
|
||||
freeze** при деградации/rollback прода (post-deploy), снимаемый вручную. Всё — аддитивно, под
|
||||
kill-switch, с областью репо, never-raise, restart-safe. Машина стадий и реестр QG **не меняются**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче | Характер изменения |
|
||||
|--------|---------------|--------------------|
|
||||
| `src/db.py` | `claim_next_job` (горячий claim), схема `tasks`/`jobs`, helper'ы выборки активной задачи репо; (возможно) аддитивная таблица/колонка для freeze | gate-условие в claim + новые read-only helper'ы + аддитивная миграция (идемпотентная, `_ensure_column`/`CREATE TABLE IF NOT EXISTS`) |
|
||||
| `src/queue_worker.py` | вызывает `claim_next_job` в `_drain_once` | без изменения контракта; gate работает внутри claim |
|
||||
| `src/webhooks/plane.py` | `start_pipeline` / `handle_status_start` / `_create_gitea_branch` | **отсрочка создания ветки** до момента, когда репо свободен (ключевое для AC-6); постановка задачи в очередь ожидания вместо немедленного среза ветки |
|
||||
| `src/git_worktree.py` | `ensure_worktree` — срез ветки от `origin/main` | гарантия: для новой задачи база = свежий `origin/main` после `git fetch` (см. §6) |
|
||||
| `src/agents/launcher.py` | `_spawn` — ленивое создание worktree на claim | согласование с отсрочкой среза ветки (не материализовать stale-ветку) |
|
||||
| `src/stage_engine.py` | `run_post_deploy_monitor` / блок `next_stage == "done"` | при вердикте деградации/rollback — выставить per-repo freeze (FR-5) |
|
||||
| `src/post_deploy.py` | `decide_action` / реакция | сигнал для freeze (`ALERT_ONLY` self / `ROLLBACK*` non-self) → выставление freeze |
|
||||
| `src/config.py` | флаги фичи | новые: `serial_gate_enabled`, `serial_gate_repos` (CSV), при необходимости — флаги freeze |
|
||||
| `src/main.py` | `GET /queue` | новый read-only блок наблюдаемости `serial_gate` |
|
||||
| `src/notifications.py` / `src/plane_sync.py` | алерты freeze | переиспользовать `send_telegram` / `set_issue_blocked` / `notify_*` (never-raise) |
|
||||
|
||||
> Чистую логику gate/freeze желательно вынести в **leaf-модуль** (например `src/serial_gate.py`,
|
||||
> never-raise, по образцу `src/task_deps.py` / `src/post_deploy.py`) — окончательно решает архитектор.
|
||||
|
||||
---
|
||||
|
||||
## 3. Функциональные изменения (требования к поведению)
|
||||
|
||||
### 3.1. FR-1 — Serial gate на входе в анализ
|
||||
- **Условие закрытия gate (per-repo):** для репо `R` gate **закрыт**, если существует задача `A` репо
|
||||
`R` со `stage != 'done'` (любая стадия `created…deploy`), **отличная** от рассматриваемой новой
|
||||
задачи `B`.
|
||||
- **Что блокируется при закрытом gate:** запуск analyst-агента новой задачи `B` **и** создание её
|
||||
ветки (Gitea-ветка + worktree). Branch у `B` не должен быть срезан, пока gate закрыт (иначе stale-base,
|
||||
AC-6).
|
||||
- **Где гейтить:** в горячем пути выбора работы — `db.claim_next_job` (по образцу `task_deps` NOT EXISTS
|
||||
gate), читая ТОЛЬКО локальную БД (NFR-2). Дополнительно — на входе `start_pipeline`, чтобы **не резать
|
||||
ветку** до открытия gate (см. §3.3).
|
||||
- **Применимость:** gate работает только для analyst-job новой задачи (вход в анализ). Job'ы уже
|
||||
активной задачи (architect/developer/…/deployer) проходят свободно — иначе единственная активная
|
||||
задача не сможет двигаться по конвейеру.
|
||||
|
||||
### 3.2. FR-2 — Очередь e2e
|
||||
- Накиданные задачи репо встают в очередь; обрабатывается строго одна end-to-end. Реализуется
|
||||
естественно: gate держит остальных, активная идёт по стадиям до `done`, затем gate открывается и
|
||||
выбирается следующая (FIFO по существующему порядку очереди `jobs.id`).
|
||||
|
||||
### 3.3. FR-1/AC-6 — Отсрочка среза ветки (анти-stale-base)
|
||||
- **Проблема (проверено):** ветка создаётся в Gitea в `start_pipeline._create_gitea_branch` от `main`
|
||||
в момент перевода issue в «To Analyse» (T0) — **до** того, как предшественник влит. `ensure_worktree`
|
||||
затем **присоединяет уже существующую** Gitea-ветку (а не режет свежую от `origin/main`), т.е. свежий
|
||||
`git fetch` не спасает — база остаётся stale.
|
||||
- **Требование:** создание ветки (Gitea-ветка и/или worktree) для новой задачи должно происходить
|
||||
**после** того, как gate открылся (предшественник `done`), чтобы базой был `origin/main`, уже
|
||||
содержащий код предшественника. Конкретный механизм отсрочки (отложить `_create_gitea_branch`;
|
||||
материализовать ветку лениво при claim'е analyst-job из свежего `origin/main`; и т.п.) — выбирает
|
||||
архитектор. Инвариант результата: **ветка `B` имеет в предках merge-commit/код всех ранее
|
||||
завершённых задач репо** (проверяемо `git merge-base --is-ancestor`).
|
||||
- Если архитектура решит резать ветку при claim'е analyst-job (а не в `start_pipeline`), это
|
||||
автоматически даёт AC-6 (claim происходит только при открытом gate).
|
||||
|
||||
### 3.4. FR-3 — Per-repo
|
||||
- Все выборки gate фильтруются по `tasks.repo` (и `jobs.repo`). Состояние gate/freeze репо `R` не
|
||||
влияет на claim/старт задач другого репо. Cross-repo параллелизм сохранён.
|
||||
|
||||
### 3.5. FR-4 — Restart-safe
|
||||
- «Активная задача репо» вычисляется запросом к БД (`tasks` по `repo` + `stage != 'done'`), не из
|
||||
in-memory. Freeze хранится в БД (аддитивная таблица/колонка). После рестарта поведение идентично.
|
||||
|
||||
### 3.6. FR-5 — Rollback-freeze
|
||||
- При вердикте post-deploy `DEGRADED` (для self — реакция `ALERT_ONLY`; для non-self с
|
||||
`post_deploy_auto_rollback` — `ROLLBACK`) для репо выставляется **durable freeze** (в БД).
|
||||
- При активном freeze репо gate **закрыт безусловно**, независимо от наличия задач `stage<done`
|
||||
(важно: деградировавшая задача к этому моменту уже `stage='done'` — BR-7 — поэтому обычный gate её
|
||||
не удержит; нужен отдельный сигнал).
|
||||
- Снятие freeze — **ручное** (оператор). Способ снятия (эндпоинт/админ-команда/ручная правка БД/
|
||||
Plane-жест) определяет архитектор; требование — снятие должно быть простым, явным и наблюдаемым.
|
||||
- Алерт: Telegram (`send_telegram`/`notify_*`) + Plane `Blocked` для деградировавшей задачи (как
|
||||
ORCH-021), плюс явное сообщение «пакет заморожен, следующая задача не стартует до ручного снятия».
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения API
|
||||
|
||||
### 4.1. Новые публичные endpoint'ы
|
||||
- **Нет обязательных новых endpoint'ов.** (Снятие freeze может быть реализовано как админ-эндпоинт —
|
||||
на усмотрение архитектора; если вводится, описать в ADR и обновить таблицу API в README.)
|
||||
|
||||
### 4.2. Изменяемые endpoint'ы
|
||||
- `GET /queue` — **аддитивно** добавляется блок `serial_gate` (read-only снимок), по образцу блоков
|
||||
`task_deps` / `reconcile` / `post_deploy`:
|
||||
- `enabled` (флаг), `repos` (область),
|
||||
- per-repo: `active_task` (`{work_item_id, stage}` или `null`), `waiting` (список ожидающих
|
||||
задач/job'ов репо), `frozen` (bool) + причина/таймстамп freeze.
|
||||
- never-raise: при ошибке — минимальный словарь с флагами и пустыми данными.
|
||||
- Контракт `GET /queue` — **расширяется аддитивно**, существующие ключи не меняются.
|
||||
|
||||
### 4.3. Webhook-обработчики
|
||||
- `start_pipeline` / `handle_status_start` (`webhooks/plane.py`): добавляется ветвление «репо занят/
|
||||
заморожен → отложить старт/срез ветки, поставить в очередь ожидания» вместо немедленного
|
||||
`_create_gitea_branch` + enqueue. Внешний контракт вебхука Plane не меняется.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
|
||||
> Только **аддитивные, идемпотентные** миграции (общая прод-БД, enduro не трогать). Без изменения
|
||||
> существующих таблиц-контрактов.
|
||||
|
||||
- **Freeze-состояние (FR-5):** требуется durable per-repo признак заморозки. Варианты (выбор —
|
||||
архитектор): новая таблица `repo_freeze(repo TEXT, frozen_at TEXT, reason TEXT, work_item_id TEXT,
|
||||
cleared_at TEXT)` **или** аддитивная колонка в существующей таблице. Требования к выбранному варианту:
|
||||
идемпотентная миграция (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), restart-safe, per-repo.
|
||||
- **Активная задача репо:** **новых колонок НЕ требуется** — вычисляется из существующих
|
||||
`tasks(repo, stage)`.
|
||||
- **Очередь ожидания:** переиспользовать существующую `jobs` (status='queued' + gate в claim) — новой
|
||||
таблицы очереди **не вводить** (FR-2 решается gate'ом, не отдельной структурой).
|
||||
- `STAGE_TRANSITIONS`, `QG_CHECKS`, `tasks`-контракт, `job_deps`, `agent_runs` — **без изменений**.
|
||||
|
||||
---
|
||||
|
||||
## 6. Требования к срезу ветки (`git_worktree` / launcher)
|
||||
- Для новой задачи, чья ветка создаётся после открытия gate: перед срезом — `git fetch origin`
|
||||
(уже есть в `ensure_worktree`), база — `origin/main` HEAD.
|
||||
- Гарантировать, что ветка НЕ присоединяется к stale Gitea-ветке, созданной раньше времени: либо не
|
||||
создавать Gitea-ветку преждевременно (отсрочка §3.3), либо при материализации worktree база
|
||||
безусловно = свежий `origin/main` (включающий предшественника).
|
||||
- Никогда не push/force-push в `main`. Существующие merge-lease / auto_rebase (ORCH-026/043) не
|
||||
трогаются.
|
||||
|
||||
---
|
||||
|
||||
## 7. Требования к новым QG checks
|
||||
- **Новых QG-проверок не вводить.** Gate — это условие планировщика (claim / старт), а **не**
|
||||
Quality Gate стадии. Реестр `QG_CHECKS` и `check_*` не меняются (как `task_deps` ORCH-026 —
|
||||
gate в claim, не новый QG).
|
||||
|
||||
## 8. Конфигурация (`src/config.py`)
|
||||
По образцу `task_deps_enabled` / `merge_gate_*` / `post_deploy_*`:
|
||||
- `serial_gate_enabled: bool = True` (env `ORCH_SERIAL_GATE_ENABLED`) — kill-switch; `False` → claim и
|
||||
старт ведут себя строго как сейчас (нулевая регрессия, NFR-4).
|
||||
- `serial_gate_repos: str = ""` (env `ORCH_SERIAL_GATE_REPOS`, CSV) — область; пусто → применять как
|
||||
по умолчанию (см. ниже).
|
||||
- Helper `serial_gate_applies(repo) -> bool` (leaf-модуль, never-raise) по образцу `post_deploy_applies`:
|
||||
`enabled` + (если CSV непуст — членство репо; иначе — область по умолчанию).
|
||||
- **Область по умолчанию (решение для ADR):** serial gate осмыслен для ВСЕХ репо (FR-3 — и orchestrator,
|
||||
и enduro выигрывают от serial e2e), в отличие от self-hosting-only гейтов (ORCH-35/43/58). Рекомендация:
|
||||
пустой CSV → применять ко всем зарегистрированным репо. Архитектор фиксирует и обосновывает в ADR.
|
||||
- При необходимости — отдельные флаги для freeze (FR-5), например `serial_gate_freeze_enabled`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Наблюдаемость и алерты
|
||||
- `GET /queue` блок `serial_gate` (см. §4.2).
|
||||
- Лог: каждое решение «gate закрыт, задача отложена» и «freeze выставлен/снят» → `logger.info/warning`.
|
||||
- Telegram: freeze (выставление) → алерт (`send_telegram`/`notify_*`); карточка задачи (ORCH-042/087)
|
||||
может отражать «⏳ ждёт завершения <work_item_id>» (по образцу строки `task_deps` «⏳ ждёт ORCH-NNN»),
|
||||
never-raise.
|
||||
|
||||
---
|
||||
|
||||
## 10. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR)
|
||||
Документация — golden source (CLAUDE.md §2). По итогам разработки обновить:
|
||||
- `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md` — решение (механизм отсрочки ветки, freeze-
|
||||
хранилище, область по умолчанию, точки врезки).
|
||||
- `docs/architecture/README.md` — новый раздел «Serial gate (ORCH-088)» + строка статуса доработок;
|
||||
обновить описание `GET /queue` (блок `serial_gate`) и раздел «База данных», если добавлена таблица.
|
||||
- `CLAUDE.md` — краткий абзац о serial-режиме (если уместно в паспорте).
|
||||
- `CHANGELOG.md` — запись `feat:`.
|
||||
- При новой таблице freeze — `docs/work-items/ORCH-088/08-data-requirements.md`.
|
||||
- При новом админ-эндпоинте снятия freeze — обновить таблицу API в README.
|
||||
|
||||
---
|
||||
|
||||
## 11. Инварианты (не нарушать)
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды deploy-хука, merge-gate (ORCH-043),
|
||||
merge-verify (ORCH-071/073), image-freshness (ORCH-058), post-deploy контракт (ORCH-021),
|
||||
`max_concurrency` — **без изменений**.
|
||||
- never-raise на единицу работы; claim fail-**open** на ошибке БД (NFR-1); freeze fail-**closed**.
|
||||
- Offline в горячем claim (NFR-2): без сетевых вызовов Plane/Gitea.
|
||||
- Не рестартить/не ронять прод-контейнер (CLAUDE.md self-hosting).
|
||||
- Миграции аддитивны и идемпотентны; enduro при выключенном/неприменимом флаге не затрагивается.
|
||||
|
||||
---
|
||||
|
||||
## 12. Открытые вопросы для архитектора (не блокируют анализ)
|
||||
- OQ-1: Механизм отсрочки среза ветки — отложить `_create_gitea_branch` в `start_pipeline` ИЛИ
|
||||
перенести материализацию ветки на claim analyst-job? (Влияет на AC-6 и на то, где живёт «ожидающая»
|
||||
задача — в Plane-статусе vs как `queued` job без ветки.)
|
||||
- OQ-2: Хранилище freeze — отдельная таблица `repo_freeze` vs колонка.
|
||||
- OQ-3: Способ ручного снятия freeze (эндпоинт / Plane-жест / админ-команда).
|
||||
- OQ-4: Поведение при задаче в Blocked/Needs-Input, держащей gate закрытым (Этап 1 — держит; нужен ли
|
||||
отдельный «вывод из учёта активных» — вероятно нет, фиксируем как осознанное).
|
||||
- OQ-5: Область по умолчанию (все репо vs только self-hosting) — рекомендация §8.
|
||||
103
docs/work-items/ORCH-088/03-acceptance-criteria.md
Normal file
103
docs/work-items/ORCH-088/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-088 — Serial gate
|
||||
|
||||
Work Item: **ORCH-088** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий — чёткое условие **PASS/FAIL**. Критерий считается выполненным, если
|
||||
описанная проверка даёт указанный результат. Нумерация AC-1…AC-6 соответствует BR; AC-7…AC-11 —
|
||||
производные/защитные.
|
||||
|
||||
> Скоп — FR-1…FR-5 (serial e2e). Merge-очередь / pre-merge rebase / фазы A/B/C — вне приёмки.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Gate закрыт при активной задаче
|
||||
**Условие:** в репо `R` есть задача `A` со `stage != 'done'`. В очередь поступает новая задача `B`
|
||||
того же репо.
|
||||
- **PASS:** analyst-агент задачи `B` НЕ запускается; ветка `B` НЕ создаётся; `B` остаётся в ожидании
|
||||
(`jobs.status='queued'` / не стартована). `GET /queue` показывает `B` как ожидающую.
|
||||
- **FAIL:** analyst `B` стартовал, или ветка `B` создана, пока `A` не `done`.
|
||||
|
||||
## AC-2 — Автостарт следующей по достижении `done`
|
||||
**Условие:** активная задача `A` репо `R` достигла `stage = 'done'` (после прод-деплоя). В очереди
|
||||
ждёт `B`.
|
||||
- **PASS:** `B` стартует анализ **автоматически** (без ручного действия) — claim analyst-job `B`
|
||||
происходит на ближайшем цикле планировщика; ветка `B` создаётся в этот момент.
|
||||
- **FAIL:** `B` не стартует после `A.stage='done'`, либо для старта требуется ручное вмешательство.
|
||||
|
||||
## AC-3 — Restart-safe (состояние в БД)
|
||||
**Условие:** активна `A` (`stage<done`), `B` ждёт; оркестратор перезапускается.
|
||||
- **PASS:** после рестарта gate по-прежнему закрыт (`B` не стартовала, `A` определяется из БД);
|
||||
после `A.stage='done'` `B` стартует. Freeze (если был выставлен) сохраняется после рестарта.
|
||||
- **FAIL:** после рестарта `B` стартовала при `A.stage<done`, или freeze «потерян».
|
||||
|
||||
## AC-4 — Per-repo параллелизм
|
||||
**Условие:** активна задача в `orchestrator` (`stage<done`); в `enduro-trails` поступает новая задача.
|
||||
- **PASS:** задача `enduro-trails` стартует анализ независимо (gate orchestrator её не держит) и
|
||||
наоборот; gate/freeze одного репо не влияет на другой.
|
||||
- **FAIL:** задача другого репо заблокирована состоянием gate/freeze чужого репо.
|
||||
|
||||
## AC-5 — Rollback-freeze + алерт
|
||||
**Условие:** задача `A` репо `R` достигла `done`; во время post-deploy мониторинга вынесен вердикт
|
||||
`DEGRADED` (self → `ALERT_ONLY`; non-self+auto_rollback → `ROLLBACK`).
|
||||
- **PASS:** для `R` выставлен durable freeze (в БД); отправлен Telegram-алерт о заморозке; следующая
|
||||
задача репо НЕ стартует, пока freeze не снят **вручную**; `GET /queue` показывает `frozen: true`.
|
||||
После ручного снятия freeze следующая задача стартует.
|
||||
- **FAIL:** следующая задача стартовала при активном freeze; либо freeze снялся автоматически; либо
|
||||
алерт не отправлен.
|
||||
|
||||
## AC-6 — Нет stale-base (ветка от свежего `main`)
|
||||
**Условие:** задачи `A` затем `B` одного репо проходят serial. `A` влита в `main` к моменту своего
|
||||
`done`.
|
||||
- **PASS:** ветка `B` срезана от `main`, **содержащего код `A`**: проверка
|
||||
`git merge-base --is-ancestor <validated_sha задачи A> <branch B>` (или равноценная: HEAD `A` в
|
||||
`main` — предок базы `B`) истинна. Branch `B` не создан раньше `A.stage='done'`.
|
||||
- **FAIL:** база `B` не содержит коммитов `A` (ветка срезана до завершения `A`).
|
||||
|
||||
## AC-7 — Kill-switch / нулевая регрессия
|
||||
**Условие:** `serial_gate_enabled = False` (или репо вне `serial_gate_repos`).
|
||||
- **PASS:** claim и старт ведут себя строго как до ORCH-088 (gate инертен); тесты прежнего поведения
|
||||
зелёные; enduro не затронут.
|
||||
- **FAIL:** при выключенном флаге поведение отличается от исходного.
|
||||
|
||||
## AC-8 — never-raise / fail-open для claim
|
||||
**Условие:** при вычислении gate происходит ошибка БД/логики в горячем пути claim.
|
||||
- **PASS:** ошибка перехвачена и залогирована; claim НЕ падает; для claim — поведение fail-open
|
||||
(очередь всех проектов не заклинивает). Конвейер enduro продолжает работать.
|
||||
- **FAIL:** ошибка gate роняет claim/воркер или заклинивает очередь.
|
||||
|
||||
## AC-9 — fail-closed для freeze
|
||||
**Условие:** при определении состояния freeze возникает сомнение/ошибка (например, не удалось
|
||||
достоверно прочитать признак).
|
||||
- **PASS:** в отношении freeze применяется консервативное (безопасное для прода) поведение — не
|
||||
стартовать следующую при невозможности подтвердить отсутствие freeze (зафиксировать в ADR/коде).
|
||||
- **FAIL:** при сомнении gate открывается и стартует следующую задачу.
|
||||
|
||||
## AC-10 — Наблюдаемость `GET /queue`
|
||||
**Условие:** запрос `GET /queue` при активной задаче и/или freeze.
|
||||
- **PASS:** ответ содержит аддитивный блок `serial_gate` с: `enabled`, областью, per-repo
|
||||
`active_task`, списком `waiting`, `frozen`. Существующие ключи `/queue` не изменены.
|
||||
- **FAIL:** блок отсутствует/ломает существующий контракт, либо данные не отражают реальное состояние.
|
||||
|
||||
## AC-11 — Инварианты неизменны
|
||||
**Условие:** проверка контрактов после внедрения.
|
||||
- **PASS:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды deploy-хука, merge-gate,
|
||||
merge-verify, image-freshness, post-deploy контракт — без изменений; миграции БД аддитивны и
|
||||
идемпотентны; прод-контейнер не рестартится механизмом gate.
|
||||
- **FAIL:** изменён любой перечисленный контракт, либо миграция не идемпотентна.
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ FR/BR
|
||||
| AC | FR | BR | Тип проверки |
|
||||
|----|----|----|--------------|
|
||||
| AC-1 | FR-1 | BR-1 | unit (claim/gate) + integration |
|
||||
| AC-2 | FR-1/2 | BR-2 | integration |
|
||||
| AC-3 | FR-4 | BR-5 | integration (restart) |
|
||||
| AC-4 | FR-3 | BR-4 | unit + integration |
|
||||
| AC-5 | FR-5 | BR-6/7 | integration |
|
||||
| AC-6 | FR-1 | BR-3 | integration (git base) |
|
||||
| AC-7 | — | BR-8 | unit |
|
||||
| AC-8 | — | NFR-1 | unit |
|
||||
| AC-9 | FR-5 | NFR-1 | unit |
|
||||
| AC-10 | — | BR-9 | unit (snapshot) |
|
||||
| AC-11 | — | NFR-5 | unit (контракты) |
|
||||
153
docs/work-items/ORCH-088/04-test-plan.yaml
Normal file
153
docs/work-items/ORCH-088/04-test-plan.yaml
Normal file
@@ -0,0 +1,153 @@
|
||||
work_item: ORCH-088
|
||||
title: "Serial gate (Этап 1: пакетный автономный режим, serial e2e)"
|
||||
scope: "FR-1..FR-5 only. Merge-queue / pre-merge rebase / phases A/B/C / ORCH-83 — out of scope."
|
||||
framework: pytest
|
||||
|
||||
# Принципы тестирования:
|
||||
# - чистую логику gate/freeze покрываем unit-тестами на leaf-функциях (без сети/БД где можно);
|
||||
# - claim-gate и e2e-последовательность — integration на временной SQLite-БД;
|
||||
# - все тесты детерминированы (без реальных Plane/Gitea/прод вызовов — мокируются);
|
||||
# - проверяем оба направления kill-switch (вкл/выкл) и never-raise.
|
||||
|
||||
tests:
|
||||
# ---------- FR-1 / AC-1: gate закрыт при активной задаче ----------
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "claim_next_job НЕ выбирает analyst-job новой задачи B, если в репо есть задача A со stage!='done' (gate закрыт)"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "serial_gate_applies(repo): enabled + пустой CSV → True для зарегистрированного репо; CSV с членством → True; репо вне CSV → False"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Job'ы УЖЕ активной задачи (architect/developer/.../deployer) gate'ом НЕ блокируются — единственная активная задача свободно идёт по конвейеру"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- FR-1/2 / AC-2: автостарт следующей по достижении done ----------
|
||||
- id: TC-04
|
||||
type: integration
|
||||
description: "После перевода A.stage='done' claim_next_job выбирает analyst-job ожидающей B того же репо (gate открылся автоматически)"
|
||||
module: tests/test_serial_gate_e2e.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: integration
|
||||
description: "Очередь из 3 задач одного репо обрабатывается строго по одной: пока A не done, ни B, ни C не стартуют; порядок FIFO по jobs.id"
|
||||
module: tests/test_serial_gate_e2e.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- FR-4 / AC-3: restart-safe ----------
|
||||
- id: TC-06
|
||||
type: integration
|
||||
description: "Активная задача определяется из БД (tasks.repo + stage!='done'), не из in-memory — после пересоздания воркера/состояния gate остаётся закрытым при A.stage<done"
|
||||
module: tests/test_serial_gate_e2e.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: integration
|
||||
description: "Freeze переживает рестарт: выставленный в БД freeze читается после пересоздания состояния; следующая задача не стартует"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- FR-3 / AC-4: per-repo ----------
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Активная задача в orchestrator (stage<done) НЕ блокирует claim analyst-job задачи в enduro-trails (gate фильтруется по repo)"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "Freeze репо orchestrator не влияет на claim/старт задач enduro-trails"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- FR-5 / AC-5: rollback-freeze + алерт ----------
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "post-deploy вердикт DEGRADED → выставляется durable per-repo freeze (запись в БД) + вызывается Telegram-алерт (send_telegram замокан, проверяется вызов)"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: integration
|
||||
description: "При активном freeze репо claim_next_job НЕ выбирает analyst-job следующей задачи, даже если нет задач stage<done (деградировавшая уже done — BR-7)"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: integration
|
||||
description: "Ручное снятие freeze → следующая задача стартует на ближайшем цикле; freeze помечается cleared в БД"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- FR-1 / AC-6: нет stale-base ----------
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: "Ветка B не создаётся (ни Gitea-ветка, ни worktree), пока gate закрыт — _create_gitea_branch/ensure_worktree для B не вызывается при A.stage<done"
|
||||
module: tests/test_serial_gate_branch.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: integration
|
||||
description: "После A.stage='done' (A влита в main) база ветки B = origin/main с кодом A: git merge-base --is-ancestor <sha A> <base B> истинно (на временном git-репо)"
|
||||
module: tests/test_serial_gate_branch.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-7: kill-switch / нулевая регрессия ----------
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "serial_gate_enabled=False → claim_next_job SQL/поведение идентичны исходным (gate инертен); B стартует независимо от A"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "Репо вне serial_gate_repos (CSV непуст) → gate не применяется к этому репо"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-8 / AC-9: never-raise ----------
|
||||
- id: TC-17
|
||||
type: unit
|
||||
description: "Ошибка БД при вычислении gate в claim → перехвачена, залогирована, claim не падает (fail-OPEN: claim продолжается)"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: unit
|
||||
description: "Ошибка при определении freeze → fail-CLOSED: следующая не стартует при невозможности подтвердить отсутствие freeze"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-10: наблюдаемость ----------
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: "serial_gate snapshot() возвращает {enabled, repos, per-repo active_task, waiting, frozen}; never-raise при ошибке → минимальный словарь"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "GET /queue содержит аддитивный блок serial_gate и НЕ меняет существующие ключи (counts/max_concurrency/reconcile/reaper/post_deploy/task_deps/recent)"
|
||||
module: tests/test_queue_endpoint.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-11: инварианты ----------
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: "STAGE_TRANSITIONS и реестр QG_CHECKS не изменены (снимок ключей совпадает с эталоном); новых QG-проверок нет"
|
||||
module: tests/test_serial_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-22
|
||||
type: unit
|
||||
description: "Миграция freeze-хранилища идемпотентна: повторный вызов init_db/_ensure не падает и не дублирует структуру"
|
||||
module: tests/test_serial_gate_freeze.py
|
||||
expected: PASS
|
||||
Reference in New Issue
Block a user