From fef5ba15d52bda61a1d25f2c36cbbf8ad3924a77 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 16 Jun 2026 17:56:23 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=763 --- docs/work-items/ORCH-124/01-brd.md | 185 ++++++++++++++++++ docs/work-items/ORCH-124/02-trz.md | 133 +++++++++++++ .../ORCH-124/03-acceptance-criteria.md | 136 +++++++++++++ docs/work-items/ORCH-124/04-test-plan.yaml | 112 +++++++++++ 4 files changed, 566 insertions(+) create mode 100644 docs/work-items/ORCH-124/01-brd.md create mode 100644 docs/work-items/ORCH-124/02-trz.md create mode 100644 docs/work-items/ORCH-124/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-124/04-test-plan.yaml diff --git a/docs/work-items/ORCH-124/01-brd.md b/docs/work-items/ORCH-124/01-brd.md new file mode 100644 index 0000000..e8383b2 --- /dev/null +++ b/docs/work-items/ORCH-124/01-brd.md @@ -0,0 +1,185 @@ +--- +work_item: ORCH-124 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +escalate: full-cycle +--- + +# 01 — BRD / Bug-report: ORCH-124 — serial gate treats Backlog/Blocked/Needs-Input paused tasks as active and blocks urgent successors + +Work Item: **ORCH-124** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в full-cycle** + +> ⚠️ **`escalate: full-cycle` (ADR-001 D5 ORCH-019).** Метка задачи — `Bug`, но по сути это +> **архитектурный** дефект: требуется **определить семантику wait/terminal состояний serial-gate** +> и выбрать механизм «пауза без блокировки» (release-on-status / явный per-task hold-флаг / +> переиспользование `task_deps`). Любой вариант пересекается с **корневым инвариантом ORCH-088 +> (анти-stale-base)** и с гармонизированным терминальным предикатом ORCH-090 (`adr-0026`, +> `serial_gate` + `task_deps` + `stages.py`). Это не «однострочная» правка — нужен ADR с явным +> разрешением конфликта свойств (см. §8 и `10-tech-risks.md` архитектора). Поэтому выпускается +> **полный** analysis-пакет (а не облегчённый bug-пакет). Оператор снимает багфикс-трек: +> `POST /bug-fast-track/escalate?work_item=ORCH-124` → задача пойдёт через стадию `architecture` +> (architect выпустит ADR для семантики паузы serial-gate). + +--- + +## 1. Бизнес-контекст и проблема + +### Симптом (наблюдаемое — установленный факт инцидента) +Во время инцидента **ORCH-116/ORCH-123**: задачу **ORCH-116** намеренно поставили на паузу +(перевели в Plane-статус Blocked/Backlog), чтобы вперёд пропустить срочный фикс **ORCH-123**. +Однако `serial_gate` **по-прежнему считал ORCH-116 активной задачей** (`active_task`) и держал +analyst-job ORCH-123 в очереди (`queued`) — срочный фикс не мог стартовать, пока ORCH-116 +формально не `done`/`cancelled`. + +### Причина симптома (установленный факт — верифицировано по коду) +`serial_gate` определяет «активную задачу репо» **исключительно по машинной стадии** +`tasks.stage NOT IN ('done','cancelled')` — в трёх местах `src/serial_gate.py`: +- `build_claim_clause()` — горячий SQL-фрагмент в `db.claim_next_job`: + `EXISTS (SELECT 1 FROM tasks t2 WHERE t2.repo = jobs.repo AND t2.id < jobs.task_id AND + t2.stage NOT IN ('done','cancelled'))`; +- `repo_has_active_task()` — Python-зеркало для наблюдаемости; +- `_per_repo_snapshot()` — выбор `active_task` для блока `serial_gate` в `GET /queue`. + +При этом **Plane-статусы Backlog / Blocked / Needs Input — это слой B (индикация), ORCH-066**, и они +**не меняют `tasks.stage` (слой A)**. Сеттеры `set_issue_blocked` / `set_issue_needs_input` +(`src/plane_sync.py`) делают только `PATCH` Plane-статуса; машинная стадия задачи остаётся прежней +(`analysis` / `development` / `deploy-staging` …). Подтверждение из кода: у таблицы `tasks` **нет +колонки статуса** (комментарий `src/reconciler.py:322`: «`tasks` has no status column, so the live +Plane state is the source of truth»). Следовательно для serial-gate приостановленная задача неотличима +от активно исполняемой — её стадия не входит в `{done,cancelled}`, значит она «активна» и блокирует +FIFO всех более поздних analyst-job того же репо. + +### Почему это важно (бизнес-боль) +- **Срочный фикс не запускается**, пока более ранняя задача поставлена на паузу. Единственные + существующие способы «разблокировать» — терминально `cancel`/довести до `done`, либо целиком + выключить serial-gate (`ORCH_SERIAL_GATE_ENABLED=false`) для всех репо. Все три — грубые. +- У оператора **нет чистого механизма «пауза без блокировки»** с явным намерением — отдельного от + отмены (терминал) и от глобального выключения гейта. +- На пакетном автономном прогоне (эпик ORCH-088) это превращает любую «отложенную» задачу в стоп-кран + очереди репо. + +### Прецедент в коде (контекст для архитектора, не решение) +Reconciler уже **умеет** уважать wait-состояния: ORCH-060 Guard 2 (`reconciler._is_blocked_or_needs_input`, +Variant A) **сетевым** запросом Plane-статуса пропускает Blocked/Needs-Input (и активные +ORCH-066-ожидания) и не «оживляет» их. Но reconciler — фоновый тик и **может** позволить себе сетевой +вызов; `serial_gate.build_claim_clause` врезан в `claim_next_job` (**offline hot-path**) и сетевого +вызова позволить **не может** (NFR-2 ниже). Это центральное расхождение, которое и порождает баг: +сигнал паузы есть в Plane, но не доступен горячему SQL гейта. + +## 2. Объём (scope) + +### В объёме +- Определить **семантику wait/terminal** для serial-gate: какие состояния задачи-предшественника + НЕ должны держать FIFO-гейт закрытым для более поздней задачи. +- Дать оператору **явный, durable, DB-резолвимый** механизм «пауза без блокировки» (или формально + переиспользовать существующий: freeze / task_deps), с чётким намерением, отличным от cancel. +- Поправить определение «активной задачи» во **всех трёх** точках `serial_gate.py`, чтобы + приостановленная задача не считалась `active_task`. +- Корректная **причина ожидания** в блоке `serial_gate` снапшота `GET /queue` + (active task / paused-predecessor / freeze / dependency). +- Тесты: предшественник Blocked/Backlog/Needs-Input + срочный успешник; регресс-тест инцидента. + +### Вне объёма +- Изменения `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключей — **не трогаем** + (маршрутизация очереди — свойство планировщика, не Quality Gate). +- Введение нового **машинного** статуса в `STAGE_TRANSITIONS` (это не новая стадия конвейера). +- Изменение поведения reconciler ORCH-060 (его networked-skip уже корректен; гармонизация — на + усмотрение архитектора, но переписывать его не требуется). +- Автоматическое управление паузой по данным вне явного намерения оператора (никакого эвристического + «само-распаузивания»). +- Конкретный **выбор механизма** (release-on-status vs per-task hold-флаг vs task_deps) — это решение + **архитектора** (ADR), а не аналитика. + +## 3. Заинтересованные стороны +- **Оператор/владелец конвейера (Слава)** — заказчик: нуждается в чистой паузе, чтобы пропускать + срочные фиксы без отмены и без выключения гейта. +- **Self-hosting orchestrator** — затрагивается напрямую (serial-gate активен для всех репо). +- **enduro-trails** — затрагивается косвенно (общая БД/очередь); регрессия недопустима при + выключенном/нейтральном поведении. +- **Архитектор** — принимает решение о механизме и семантике (ADR), разрешает конфликт §8. +- Принимает результат — reviewer + tester по критериям `03-acceptance-criteria.md`. + +## 4. Бизнес-требования (BR) +- **BR-1** — Перевод задачи-предшественника в состояние паузы/ожидания (Backlog / Blocked / + Needs Input) **больше не должен случайно блокировать** более позднюю срочную задачу того же репо в + serial-gate. Проверяемо: analyst-job успешника становится claimable. +- **BR-2** — У оператора есть **чистый механизм «пауза без блокировки»** с явным намерением, + **отличный** от `cancel` (терминал) и от глобального выключения гейта. Намерение — durable + (переживает рестарт процесса/контейнера). +- **BR-3** — Пауза снимает гейт **только по явному намерению**. **Нормально исполняемая** задача + (реально идёт работа) **по-прежнему держит** гейт — анти-stale-base гарантия ORCH-088 не + регрессирует (см. §8 — конфликт свойств, разрешает архитектор). +- **BR-4** — Снапшот `serial_gate` в `GET /queue` показывает **корректную причину** ожидания + успешника: активная задача / приостановленный предшественник / freeze / dependency. +- **BR-5** — При **возобновлении** (распаузе) задачи serial-ordering корректно восстанавливается: + возобновлённая задача снова участвует в гейте (либо держит его, либо явно ре-входит в FIFO с + обязательством rebase) — нет «вечного обхода» и нет потерянного намерения. +- **BR-6** — Существующие гарантии serial-gate сохранены: FIFO по более ранним незавершённым + задачам, durable per-repo `freeze` (`repo_freeze`), cross-repo параллелизм, явные `task_deps` — + по-прежнему блокируют, где должны. + +## 5. Нефункциональные требования (NFR) +- **NFR-1 (never-raise / fail-safe)** — Контракт leaf `serial_gate` сохранён: каждая публичная + функция деградирует консервативно. Сохранить два направления отказа ORCH-088: hot-claim build → + **fail-OPEN** (`""`-фрагмент, не заклинить очередь всех проектов, AC-8 ORCH-088); freeze-решение → + **fail-CLOSED** (прод-безопасность, AC-9 ORCH-088). Новая логика паузы не должна инвертировать эти + направления. +- **NFR-2 (чистота hot-path)** — Гейт-в-claim остаётся **offline SQL-предикатом**; **никаких сетевых + вызовов** (в т.ч. Plane API) в `claim_next_job`. Сигнал «пауза» обязан быть **DB-резолвимым** + (durable колонка/таблица), а не считываться из Plane на горячем пути (в отличие от reconciler). +- **NFR-3 (обратная совместимость / kill-switch / область)** — Изменение аддитивно и обратимо; + выключатель (существующий `serial_gate_enabled` либо новый под-флаг) → байт-в-байт прежнее поведение + до ORCH-124. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / **схемы + существующих таблиц** — без изменений (новая колонка/таблица — только аддитивно, паттерн + `_ensure_column` / `CREATE TABLE IF NOT EXISTS`). enduro не затронут при нейтральном поведении. +- **NFR-4 (гармонизация предиката)** — Любой новый предикат «активна/терминальна/пауза» должен + оставаться согласованным с гармонизированным терминальным множеством `{done,cancelled}` в + `serial_gate` + `task_deps` + `stages.py` (ORCH-090 / adr-0026), либо архитектор явно описывает, + почему serial-gate расходится (паузой), не ломая `task_deps`. +- **NFR-5 (self-hosting безопасность)** — Никакого рестарта/падения прод-контейнера, мутации `main`, + force-push; только чтение/запись своих таблиц и принятие решения о claim. Hot-path не должен + замедляться сетью или тяжёлым запросом. + +## 6. Допущения и ограничения +- У таблицы `tasks` **сегодня нет колонки статуса**; Plane-статус (Backlog/Blocked/Needs Input) — + слой B индикации, в БД не отражён. Значит «пауза» для горячего пути требует **нового durable + DB-сигнала** (колонка `tasks` или отдельная таблица), либо переиспользования уже DB-резолвимого + механизма (`repo_freeze` / `task_deps`). +- `repo_freeze` существует, но **freeze'ит весь репо** (блокирует всех успешников) — это + противоположность «пропустить срочного успешника», поэтому как есть не годится для BR-1 (но годится + как явный блок для BR-6). +- `task_deps` (`job_deps`) — явные декларации зависимостей, уже DB-резолвимы и консультируются в + `claim_next_job` (`NOT EXISTS`); кандидат на «explicit intent», на усмотрение архитектора. +- Reconciler ORCH-060 различает Blocked/Needs-Input **сетевым** запросом Plane — прецедент семантики, + но **не переиспользуем** на hot-path (NFR-2). +- Серый кейс: Needs Input во время `analysis` — нормальное короткое ожидание ответа; решение, считать + ли его «паузой для гейта», за архитектором (важно не превратить штатное короткое ожидание в обход + анти-stale-base). + +## 7. Критерии успеха +Кратко (детальные PASS/FAIL — `03-acceptance-criteria.md`): +- Приостановленный предшественник (Backlog/Blocked/Needs-Input по явному намерению) не блокирует + срочного успешника; нормально идущая задача — блокирует; freeze/dependency блокируют, где должны; + `GET /queue` показывает корректную причину; всё под kill-switch; машинные инварианты байт-в-байт; + регресс-тест инцидента красный до фикса и зелёный после. + +## 8. Риски +Кратко (детально — `10-tech-risks.md`, заполняет архитектор): +- **R-1 (ключевой конфликт свойств) — пауза vs анти-stale-base (ORCH-088).** Если «пауза» освобождает + гейт, успешник срежет ветку от `main`, ещё **не** содержащего код предшественника. Когда + приостановленный предшественник позже возобновится и смержится — его база/ветка могут стать stale. + ORCH-088 был построен ровно чтобы это предотвратить. Архитектор обязан разрешить конфликт явно + (напр.: пауза «демотирует» задачу в FIFO и обязывает rebase при возобновлении; либо явный per-task + «yield» с принятием rebase). **Аналитик фиксирует конфликт, не выбирает решение.** +- **R-2** — Случайное/неявное освобождение гейта (баг в детекте намерения) ослабит сериализацию для + всех — требуется строго **явное** намерение оператора. +- **R-3** — Рассинхрон «Plane-статус ↔ DB-сигнал паузы»: если механизм опирается на webhook о смене + статуса, потерянный webhook оставит задачу «активной» в БД (или наоборот). Нужен durable, + идемпотентный, восстановимый сигнал. +- **R-4** — Регрессия гармонизированного предиката `{done,cancelled}` в `task_deps`/`stages.py`, если + serial-gate изменит понимание «активности» неаккуратно. +- **R-5** — fail-direction: ошибка в новой ветке не должна инвертировать fail-OPEN (claim) / + fail-CLOSED (freeze) контракты ORCH-088. diff --git a/docs/work-items/ORCH-124/02-trz.md b/docs/work-items/ORCH-124/02-trz.md new file mode 100644 index 0000000..611963b --- /dev/null +++ b/docs/work-items/ORCH-124/02-trz.md @@ -0,0 +1,133 @@ +--- +work_item: ORCH-124 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +escalate: full-cycle +--- + +# 02 — ТЗ (TRZ): ORCH-124 — wait/terminal-семантика serial-gate (пауза без блокировки) + +Work Item: **ORCH-124** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **требования к изменениям**, выведенные из BRD и фактического кода. **Выбор +> механизма** «паузы без блокировки» (release-on-status / per-task hold-флаг / task_deps) и его +> архитектурное обоснование — задача **архитектора** (`06-adr`, эскалация в full-cycle). Ниже — +> что должно стать истинным и какие модули это затронет, без предписания «как именно». + +## 1. Сводка изменения +Сейчас serial-gate (`src/serial_gate.py`) считает «активной» любую задачу репо со стадией вне +`{done,cancelled}` — в трёх точках (`build_claim_clause` / `repo_has_active_task` / +`_per_repo_snapshot`). Поскольку Plane-статусы Backlog/Blocked/Needs-Input (слой B индикации) **не +меняют `tasks.stage`** (слой A), приостановленная задача неотличима от активной и держит FIFO-гейт +закрытым для более поздних analyst-job. Требуется ввести **явный, durable, DB-резолвимый** признак +«пауза/park» и научить определение «активной задачи» его учитывать, **сохранив** анти-stale-base +ORCH-088 при возобновлении (R-1). Машинные гейты не трогаются — это правка **планировщика очереди**. + +## 2. Задействованные модули / пути +| Путь | Действие | +|------|----------| +| `src/serial_gate.py` | изменить — определение «активной задачи» во всех 3 точках (`build_claim_clause`, `repo_has_active_task`, `_per_repo_snapshot`); причина ожидания в снапшоте | +| `src/db.py` | изменить (вероятно) — `claim_next_job` (учёт нового предиката в горячем SQL) + (если выбран DB-сигнал) аддитивная колонка/таблица `_ensure_column`/`CREATE TABLE IF NOT EXISTS` + read-only/мутатор-хелперы | +| `src/config.py` | изменить — под-флаг(и) семантики паузы (kill-switch), область репо (по образцу `serial_gate_*`) | +| `src/main.py` | изменить (вероятно) — операторский эндпоинт pause/resume **или** расширение блока `serial_gate` в `GET /queue` причиной ожидания | +| `src/webhooks/plane.py` и/или `src/plane_sync.py` | изменить (если механизм = release-on-status) — захват намерения паузы из смены Plane-статуса в durable DB-сигнал (не на hot-path) | +| `tests/test_serial_gate*.py` (новый `tests/test_orch124_serial_gate_pause.py`) | создать/дополнить — кейсы паузы + регресс инцидента | +| `docs/architecture/README.md`, `CHANGELOG.md`, `docs/work-items/ORCH-124/06-adr/` | обновить — раздел serial-gate + ADR (архитектор) | + +> Точный набор модулей зависит от выбранного механизма (ADR). Минимально-необходимый набор — +> `serial_gate.py` (3 точки) + `db.py` (hot-path) + `config.py` (флаг) + тесты; остальное — по решению +> архитектора. + +## 3. Функциональные требования + +### FR-1 — Признак паузы исключает задачу из «активных» в горячем SQL (BR-1, NFR-2) +`build_claim_clause()`: подзапрос `EXISTS (... t2.stage NOT IN ('done','cancelled'))` должен +**дополнительно** исключать приостановленные задачи-предшественники. Предикат — **чисто SQL по +локальной БД** (никакой сети). Форма исключения — функция выбранного DB-сигнала (доп. колонка +`tasks.` / отдельная таблица hold / `task_deps`): архитектор фиксирует точную SQL-форму в ADR. +Инвариант: job'ы уже активной задачи (`agent != 'analyst'`) проходят как раньше; самоблокировки +собственной строки (R-7 ORCH-088) нет. + +### FR-2 — Зеркало и снапшот согласованы с FR-1 (BR-1/BR-4) +`repo_has_active_task()` и `_per_repo_snapshot()` используют **тот же** предикат «активна», что и +`build_claim_clause` — приостановленный предшественник не попадает в `active_task`. Все три точки +правятся согласованно (анти-дрейф: они должны давать один ответ на один вход). + +### FR-3 — Явное durable намерение паузы (BR-2, BR-5, NFR-2) +Должен существовать **явный** оператор-инициируемый сигнал «park/pause» задачи, **durable** +(переживает рестарт) и **DB-резолвимый**. Распауза (resume) — обратная операция, восстанавливающая +участие задачи в serial-gate (BR-5). Сигнал **отличен** от `cancel`/`done` (не терминальный) и от +глобального kill-switch. Конкретная форма (новый эндпоинт `POST /serial-gate/pause|resume`, или +маппинг Plane-статуса в DB-сигнал через webhook, или декларация `task_deps`) — **решение архитектора**. + +### FR-4 — Анти-stale-base при возобновлении (BR-3, R-1 — критично) +Решение обязано **не регрессировать** ORCH-088: нормально исполняемая задача держит гейт; а +возобновлённая ранее приостановленная задача не должна приводить к stale-базе у успешника, который +прошёл вперёд. Механизм разрешения (демотирование в FIFO + обязательный rebase при resume / явный +yield с принятием rebase / иное) фиксируется архитектором в ADR. ТЗ требует лишь: после распаузы +ни одна из задач не остаётся на устаревшей базе незаметно. + +### FR-5 — Корректная причина ожидания (BR-4) +Блок `serial_gate` в `GET /queue` для ожидающего успешника различает причины: `active-task` +(идёт работа) / `paused-predecessor` (предшественник на паузе — не должно случаться после фикса, но +наблюдаемо) / `freeze` / `dependency`. Аддитивно к существующим ключам снапшота (`active_task` / +`waiting` / `frozen` / `frozen_reason` / `frozen_at`). + +### FR-6 — Явные блоки сохранены (BR-6) +`repo_freeze` (durable per-repo freeze) и `task_deps` (`job_deps`) продолжают блокировать claim ровно +как сейчас. «Пауза» НЕ должна обходить freeze/dependency — это разные оси (freeze = весь репо; +dependency = конкретная пара; пауза = «пропустите меня»). + +### FR-7 — Условность и нейтральность (NFR-3) +Поведение под выключателем (существующий `serial_gate_enabled` либо новый под-флаг паузы) → байт-в-байт +до ORCH-124. Область репо — по образцу `serial_gate_repos` (CSV; пусто → текущая область serial-gate, +т.е. все репо). enduro не затронут при нейтральном поведении. + +## 4. Изменения API +Вероятно (на усмотрение архитектора, ADR): +- **Новые операторские эндпоинты** `POST /serial-gate/pause?work_item=` и + `POST /serial-gate/resume?work_item=` (по образцу существующего `POST /serial-gate/unfreeze`), + если механизм = явный per-task hold. Возвращают новое состояние (paused/active) задачи. +- **Расширение** `GET /queue` → блок `serial_gate` дополняется причиной ожидания (FR-5) и (если есть) + списком приостановленных задач репо. Существующие ключи не меняются (BC). + +Если механизм = release-on-status, новых ручных эндпоинтов может не быть (намерение — смена Plane-статуса, +захватываемая webhook'ом в durable DB-сигнал); тогда раздел сводится к расширению `GET /queue`. + +## 5. Изменения схемы БД +Вероятно **аддитивно** (на усмотрение архитектора): +- Колонка `tasks.paused_at TEXT` (NULL = не на паузе), идемпотентно через `_ensure_column` — паттерн + существующих `tasks.cancelled_at` / `tasks.cancel_requested_at` / `tasks.track`; **или** +- Отдельная таблица hold (по образцу `repo_freeze`: `work_item_id`/`task_id`, `paused_at`, + `cleared_at IS NULL` = активна), `CREATE TABLE IF NOT EXISTS`. +Схемы **существующих** таблиц (`tasks`/`jobs`/`job_deps`/`repo_freeze`) — без деструктивных изменений. +Если механизм = task_deps, новой схемы может не понадобиться вовсе. Финальное решение — ADR + +`08-data-requirements.md` (архитектор). + +## 6. Требования к новым/изменённым QG checks +**Нет.** `STAGE_TRANSITIONS` / состав `QG_CHECKS` / семантика и имена `check_*` / machine-verdict ключи +(`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`) — **байт-в-байт не +трогаются**. ORCH-124 — правка планировщика очереди (claim) и наблюдаемости, **не** Quality Gate и +**не** новая стадия конвейера. + +## 7. Совместимость / регресс +- **Обратная совместимость:** аддитивно; выключатель → поведение до ORCH-124 байт-в-байт (NFR-3, + FR-7). Существующие тесты serial-gate (`tests/test_serial_gate*.py`) остаются зелёными. +- **Kill-switch / область:** переиспользовать `serial_gate_enabled` (kill-switch) + при необходимости + ввести под-флаг семантики паузы (независимый тумблер, по образцу `serial_gate_freeze_enabled`) и + область `*_repos` (CSV). +- **Обратимость:** выставить под-флаг паузы в `False` → serial-gate работает как ORCH-088/090 + (приостановленные снова считаются активными — т.е. возврат к текущему багу, но это осознанный + rollback-режим, не дефолт). +- **never-raise / fail-direction (NFR-1):** сохранить fail-OPEN на hot-claim build и fail-CLOSED на + freeze; новая ветка паузы не инвертирует эти направления. +- **Гармонизация предиката (NFR-4):** не сломать `task_deps`/`stages.py` терминальное множество + `{done,cancelled}` (ORCH-090/adr-0026); расхождение serial-gate (паузой) — только осознанно и + задокументированно в ADR. +- **Артефакты pipeline:** обновляются `docs/architecture/README.md` (раздел serial-gate ORCH-088 + + семантика паузы), `CHANGELOG.md`, новый `docs/work-items/ORCH-124/06-adr/ADR-001-*.md` (архитектор), + при необходимости `08-data-requirements.md` (архитектор). Reviewer проверяет обновление доки (правило + агентов §6). diff --git a/docs/work-items/ORCH-124/03-acceptance-criteria.md b/docs/work-items/ORCH-124/03-acceptance-criteria.md new file mode 100644 index 0000000..c0c4d44 --- /dev/null +++ b/docs/work-items/ORCH-124/03-acceptance-criteria.md @@ -0,0 +1,136 @@ +--- +work_item: ORCH-124 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +escalate: full-cycle +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-124 — wait/terminal-семантика serial-gate + +Work Item: **ORCH-124** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что считается +провалом). Reviewer/tester проверяет их буквально по файлам репозитория и тестам. + +--- + +## AC-1 — Приостановленный предшественник не блокирует срочного успешника (регресс инцидента) + +**Условие:** репо имеет более раннюю задачу A (`A.id < B.id`) в состоянии паузы (Blocked/Backlog/ +Needs-Input по явному намерению оператора) и более позднюю срочную задачу B с queued analyst-job. +- **PASS:** `build_claim_clause()` НЕ блокирует analyst-job задачи B; `claim_next_job()` выбирает его; + B входит в `analysis`. Тест воспроизводит инцидент ORCH-116/ORCH-123 (красный до фикса, зелёный + после). +- **FAIL:** analyst-job B остаётся `queued`, потому что приостановленная A всё ещё считается активной. + +--- + +## AC-2 — Нормально исполняемая задача по-прежнему держит гейт (анти-stale-base, без регресса ORCH-088) + +**Условие:** репо имеет более раннюю задачу A, **реально исполняемую** (не на паузе, стадия вне +`{done,cancelled}`) и более позднюю задачу B с queued analyst-job. +- **PASS:** analyst-job B **остаётся заблокированным** (FIFO ORCH-088 цел); B стартует только когда A + становится `done`/`cancelled` (или явно поставлена на паузу оператором). +- **FAIL:** B стартует поверх ещё не завершённой активной A → возврат stale-base дефекта, который + закрывал ORCH-088. + +--- + +## AC-3 — Пауза снимает гейт только по явному durable намерению + +**Условие:** способ перевода задачи в «паузу» для serial-gate. +- **PASS:** освобождение гейта происходит **только** при явном операторском сигнале (эндпоинт + pause / выбранный механизм ADR), сигнал **durable** (переживает рестарт процесса/контейнера) и + **DB-резолвимый**. Никакого эвристического само-распаузивания. +- **FAIL:** гейт снимается без явного намерения (например, по умолчанию для любой не-`done` задачи) ИЛИ + намерение теряется после рестарта. + +--- + +## AC-4 — Анти-stale-base при возобновлении (R-1 разрешён архитектором) + +**Условие:** приостановленная A возобновлена после того, как успешник B уже прошёл вперёд. +- **PASS:** ни A, ни B не остаются незаметно на устаревшей базе — выбранный механизм (демотирование A + в FIFO + обязательный rebase / явный yield с rebase / иное по ADR) гарантирует, что возобновлённая + задача строится/мержится на актуальном `origin/main`. Поведение зафиксировано тестом по контракту ADR. +- **FAIL:** возобновлённая A или прошедший B приводит к stale-ветке/затиранию кода без сигнала. + +--- + +## AC-5 — Явные блоки (freeze + dependency) сохранены + +**Условие:** активный `repo_freeze` для репо ИЛИ объявленная незавершённая зависимость в `job_deps`. +- **PASS:** claim успешника по-прежнему **заблокирован** freeze'ем (до ручного + `POST /serial-gate/unfreeze`) и/или незавершённой зависимостью (`task_deps`) — пауза их не обходит. +- **FAIL:** «пауза» позволяет обойти freeze или объявленную зависимость. + +--- + +## AC-6 — Корректная причина ожидания в `GET /queue` + +**Условие:** блок `serial_gate` в снапшоте `GET /queue`. +- **PASS:** для ожидающего успешника видна корректная причина ожидания (`active-task` / + `paused-predecessor` / `freeze` / `dependency`); приостановленный предшественник НЕ показывается как + `active_task`. Существующие ключи снапшота не сломаны (BC). +- **FAIL:** снапшот показывает приостановленную задачу как `active_task` ИЛИ причина ожидания неверна/ + отсутствует. + +--- + +## AC-7 — Hot-path остаётся offline (без сети) + +**Условие:** путь `db.claim_next_job` → `serial_gate.build_claim_clause`. +- **PASS:** определение «активна/пауза» резолвится **только** локальной БД (SQL); ни одного сетевого + вызова (Plane API и т.п.) на горячем пути claim. Проверяемо тестом (claim работает без сети / без + Plane). +- **FAIL:** claim делает сетевой вызов для определения паузы. + +--- + +## AC-8 — Машинные инварианты байт-в-байт; kill-switch off → прежнее поведение + +**Условие:** реестры и выключатель. +- **PASS:** `STAGE_TRANSITIONS` / состав `QG_CHECKS` / имена и семантика `check_*` / machine-verdict + ключи / схемы существующих таблиц — без изменений (структурный тест). При выключенном под-флаге паузы + serial-gate ведёт себя байт-в-байт как ORCH-088/090. +- **FAIL:** изменён любой machine-verdict ключ / состав `QG_CHECKS` / `STAGE_TRANSITIONS`, либо + выключенный флаг меняет поведение. + +--- + +## AC-9 — never-raise и сохранённые fail-directions + +**Условие:** ошибки внутри новой логики паузы. +- **PASS:** все публичные функции `serial_gate` never-raise; hot-claim build по-прежнему **fail-OPEN** + (`""`-фрагмент при ошибке), freeze-решение по-прежнему **fail-CLOSED**; новая ветка паузы не + инвертирует эти направления. Проверяемо тестом (инъекция ошибки → claim не падает, очередь не + заклинивает). +- **FAIL:** ошибка в ветке паузы роняет claim/worker ИЛИ инвертирует fail-direction. + +--- + +## AC-10 — Возобновление восстанавливает участие в гейте (BR-5) + +**Условие:** распауза ранее приостановленной задачи. +- **PASS:** после resume задача снова участвует в serial-gate согласно выбранной семантике (держит + гейт как активная либо ре-входит в FIFO); нет «вечного обхода». +- **FAIL:** возобновлённая задача навсегда остаётся вне гейта / её намерение паузы «залипает». + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1, FR-2 | +| AC-2 | BR-3 / FR-4 | +| AC-3 | BR-2 / FR-3 | +| AC-4 | BR-3, BR-5 / FR-4 | +| AC-5 | BR-6 / FR-6 | +| AC-6 | BR-4 / FR-5 | +| AC-7 | NFR-2 / FR-1 | +| AC-8 | NFR-3, NFR-4 / FR-7 | +| AC-9 | NFR-1 / FR-7 | +| AC-10 | BR-5 / FR-3 | diff --git a/docs/work-items/ORCH-124/04-test-plan.yaml b/docs/work-items/ORCH-124/04-test-plan.yaml new file mode 100644 index 0000000..62cf9d3 --- /dev/null +++ b/docs/work-items/ORCH-124/04-test-plan.yaml @@ -0,0 +1,112 @@ +work_item: ORCH-124 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +title: "Serial-gate wait/pause semantics — paused predecessor must not block urgent successor" +framework: pytest +scope: > + Покрывает определение "активной задачи" в serial-gate (build_claim_clause / + repo_has_active_task / _per_repo_snapshot), durable явный сигнал паузы и его учёт в + горячем SQL claim, сохранность анти-stale-base ORCH-088, явных блоков (freeze/task_deps), + наблюдаемости GET /queue, never-raise/fail-directions и kill-switch. Вне покрытия: + изменения STAGE_TRANSITIONS/QG_CHECKS/check_* (их НЕТ), сетевые вызовы Plane API на hot-path + (запрещены — проверяется их ОТСУТСТВИЕ), реальный прод-деплой/рестарт. +notes: > + TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс-тест инцидента ORCH-116/ORCH-123: красный до фикса, зелёный + после. Точные имена флагов/колонок/эндпоинтов и SQL-форма предиката зависят от выбранного + механизма (ADR архитектора) — тест-план фиксирует ПОВЕДЕНИЕ, не реализацию. Тесты + работают на временной SQLite-БД без сети/Plane/прода (паттерн tests/test_serial_gate*.py). + Полный регресс tests/ должен оставаться зелёным (особенно test_serial_gate*.py). + +tests: + - id: TC-01 + type: integration + description: "РЕГРЕСС (обязательный): репо с более ранней приостановленной задачей A + более поздняя срочная B → claim_next_job выбирает analyst-job B (гейт открыт). Красный до фикса, зелёный после. Воспроизводит ORCH-116/ORCH-123." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-02 + type: unit + description: "Предшественник в Backlog (по явному намерению) не считается активным: build_claim_clause не блокирует analyst-job успешника." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-03 + type: unit + description: "Предшественник в Needs-Input (по явному намерению) не блокирует успешника — параллельно AC-1 для иного wait-состояния." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-04 + type: unit + description: "АНТИ-РЕГРЕСС ORCH-088: нормально исполняемый предшественник (не на паузе, стадия вне done/cancelled) ПО-ПРЕЖНЕМУ блокирует analyst-job успешника (FIFO цел)." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-05 + type: unit + description: "Пауза требует явного durable намерения: задача без операторского сигнала паузы остаётся активной (гейт держится); сигнал паузы DB-резолвим." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-06 + type: unit + description: "Durable: сигнал паузы переживает пересоздание соединения/рестарт (читается из БД, не из памяти процесса)." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-07 + type: unit + description: "Возобновление (resume) восстанавливает участие задачи в serial-gate (держит гейт / ре-входит в FIFO согласно ADR); нет вечного обхода." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-08 + type: unit + description: "Явные блоки сохранены: активный repo_freeze продолжает блокировать claim успешника; пауза freeze не обходит." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-09 + type: unit + description: "Явные блоки сохранены: незавершённая объявленная зависимость (job_deps/task_deps) продолжает блокировать claim; пауза dependency не обходит." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-10 + type: unit + description: "GET /queue snapshot: приостановленный предшественник НЕ показывается как active_task; причина ожидания успешника корректна (active-task/paused-predecessor/freeze/dependency); существующие ключи snapshot сохранены." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-11 + type: unit + description: "Согласованность трёх точек: build_claim_clause, repo_has_active_task и _per_repo_snapshot дают один и тот же вердикт 'активна' на одном входе (анти-дрейф)." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-12 + type: unit + description: "Hot-path offline: claim_next_job + build_claim_clause резолвят паузу без сетевого вызова (Plane API не вызывается); claim работает при недоступном Plane." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-13 + type: unit + description: "never-raise / fail-directions: инъекция ошибки в логику паузы → build_claim_clause fail-OPEN ('' фрагмент, очередь не клинит), freeze-решение fail-CLOSED; ни одна публичная функция не бросает." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-14 + type: unit + description: "Kill-switch / нейтральность: при выключенном под-флаге паузы (и/или serial_gate_enabled) поведение serial-gate байт-в-байт как ORCH-088/090; вне области репо — не затронуто (enduro)." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS + + - id: TC-15 + type: unit + description: "Структурный анти-регресс: STAGE_TRANSITIONS, состав QG_CHECKS, имена check_* и machine-verdict ключи не изменены; схемы существующих таблиц tasks/jobs/job_deps/repo_freeze не сломаны." + module: tests/test_orch124_serial_gate_pause.py + expected: PASS