# 02-TRZ — ORCH-086: терминал-скип и dedup на пути F-1 реконсилятора

> Техническое задание. Архитектурное решение (КАК именно) — за архитектором (ADR). Здесь — ЧТО должно измениться и инварианты.

## 1. Задействованные модули `src/`
- **`src/reconciler.py`** — основной (и, как ожидается, единственный) изменяемый модуль:
  - `Reconciler._reconcile_gate_task` (стр.180–228) — путь F-1, где находится баг.
  - `Reconciler._note_unblock` (стр.444–477) — точка отправки уведомления + dedup-guard.
  - `Reconciler._is_terminal_state` (стр.327–344) — существующий терминал-детект (сейчас зовётся только из F-2); переиспользуется в F-1.
  - `Reconciler._is_blocked_or_needs_input` (стр.230–288) — уже делает `fetch_issue_state`; желательно переиспользовать его результат, чтобы не удваивать сетевой вызов.
- **Возможно затрагиваемые (read-only переиспользование, без изменения контракта):** `src/plane_sync.py` (`fetch_issue_state`, `get_project_states`, `get_project_state_groups`), `src/projects.py` (`get_project_by_repo`). Изменять их не требуется.
- **НЕ затрагиваются:** `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`), `src/stage_engine.py` (`advance_stage`/`advance_if_gate_passed`), `src/db.py` (схема), `src/config.py` (новые флаги не вводятся).

## 2. Требуемые изменения (функциональные)

### TR-1 (G2): терминал-скип на пути F-1
В `_reconcile_gate_task` ДО вызова `_note_unblock` (а лучше — до/вместо доведения терминальной задачи до `advance_if_gate_passed`) добавить проверку: **является ли задача терминальной**.
- Терминальность определяется тем же способом, что и в F-2 (`_is_terminal_state`): первичный дискриминатор — **группа статуса Plane** issue ∈ `{completed, cancelled}`; fallback (группа недоступна) — логические ключи `done`/`cancelled` проекта. Это покрывает грабли R1 (enduro vs orchestrator).
- Дополнительно: терминальной считается и задача, чья **стадия в БД оркестратора** ∈ `{done, cancelled}` (на случай дрейфа Plane↔БД; `get_active_tasks_for_reconcile` уже отсекает `done`, но `cancelled` — нет).
- Терминальная задача → **return без advance и без `_note_unblock`**; инкремент `self.skipped_terminal_total` (единая семантика с F-2, стр.363).
- Скип **безусловный** (как терминал-скип F-2 — без отдельного kill-switch). Это НЕ маскирует легитимный replay: реально застрявшая задача терминальной в Plane не бывает.

> **Где именно** ставить проверку (до `advance_if_gate_passed` или внутри/перед `_note_unblock`) — решает архитектор. Рекомендация: ставить как ранний guard в `_reconcile_gate_task` рядом с Guard 1/Guard 2 (чтобы терминальная задача даже не запускала `advance_if_gate_passed`/гейт). Если терминал-детект требует Plane-статус, он логично переиспользует fetch из Guard 2.

### TR-2 (G3): проброс `state_uuid` в `_note_unblock` из F-1
Вызов на стр.228 должен передавать `state_uuid` (текущий Plane-state issue), чтобы in-memory dedup-guard (`_unblock_dedup`, стр.459–463) работал и на пути F-1:
```python
# было:
self._note_unblock(task.get("work_item_id") or str(task_id), stage)
# должно (концептуально):
self._note_unblock(task.get("work_item_id") or str(task_id), stage, state_uuid)
```
- `state_uuid` — текущий uuid статуса issue в Plane (тот же, что используется для терминал-детекта TR-1).
- Если Plane недоступен и `state_uuid` достоверно получить нельзя → допустимо передать `None` (dedup деградирует в no-op, как сегодня), НО приоритетно сначала отрабатывает терминал-скип TR-1; never-raise сохраняется.
- **Сигнатуру `_note_unblock` не менять** (3-й параметр `state_uuid` уже опциональный, стр.445).

### TR-3: переиспользование сетевого вызова (R4, нефункц., желательно)
F-1 не должен делать > 1 обращения к Plane API на задачу за тик ради статуса. `_is_blocked_or_needs_input` уже вызывает `fetch_issue_state`. Архитектор решает форму переиспользования (например, вынести резолв `(project_states, groups, current_state_uuid)` в один helper, питающий Guard 2 + терминал-скип TR-1 + dedup TR-2). Допустимо и без рефакторинга, если число вызовов на тик не растёт значимо.

## 3. Контракты и инварианты (НЕ нарушать)
- **never-raise:** каждая единица работы F-1 изолирована (`_reconcile_gate_task` уже под `try/except` в `reconcile_gate_once`, стр.162–168). Любая ошибка терминал-детекта/fetch → не падает тик; консервативное поведение (R3): при невозможности достоверно определить терминальность — НЕ слать ложно, но и не глушить легитимный (см. AC-4: легитимный unblock — это реальная смена стадии не-терминальной задачи; терминал-неопределённость к нему не относится).
- **silence-when-in-sync:** терминальная (= полностью синхронизированная) задача → тишина (инвариант ORCH-068 AC-1/AC-2, теперь и для F-1).
- **Легитимный unblock сохраняется:** не-терминальная реально застрявшая задача с зелёным гейтом по-прежнему `advance` + уведомление (AC-4).
- **Наблюдаемость ORCH-068:** `skipped_terminal_total` инкрементируется при терминал-скипе F-1; `deduped_total` — при подавлении повтора dedup'ом; `unblocked_total`/`last_unblocked` — только при реальной отправке. Снимок `status()` (стр.516–528) и блок `reconcile` в `GET /queue` — без структурных изменений.
- **Условность мультипроекта:** терминал-детект работает и для enduro, и для orchestrator (по группе статуса + fallback). Пайплайн/статусы enduro не трогаются.

## 4. Изменения API
Нет. HTTP-эндпоинты не меняются. `GET /queue` блок `reconcile` сохраняет форму (значения счётчиков — наблюдаемое поведение).

## 5. Изменения схемы БД
Нет. Миграции нет. (Терминальность Plane резолвится онлайн, как в ORCH-068 / Guard 2 — Вариант A без колонки статуса в `tasks`.)

## 6. Новые/изменённые QG checks
Нет. Реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются.

## 7. Конфигурация
Новые флаги НЕ вводятся. Терминал-скип безусловен (как у F-2). Существующие `reconcile_enabled`, `reconcile_notify_unblock`, `reconcile_skip_blocked_enabled`, `reconcile_plane_enabled` — без изменений семантики. (`reconcile_skip_blocked_enabled` гейтит ТОЛЬКО Guard 2; терминал-скип TR-1 ему НЕ подчиняется.)

## 8. Артефакты pipeline, подлежащие обновлению (документация = golden source)
- `docs/architecture/README.md` — раздел «Reconciler … ORCH-068»: дописать, что терминал-исключение и dedup теперь покрывают и F-1 (gate-side), не только F-2.
- `CHANGELOG.md` — запись `fix:` про ORCH-086.
- `docs/work-items/ORCH-086/06-adr/ADR-NNN-*.md` — ADR (создаёт архитектор).
- (Опционально) краткая ссылка в ADR ORCH-068, что F-1-пробел закрыт ORCH-086.

## 9. Готовность к development (Definition of Ready)
- G1 подтверждён по prod-логам/БД (точная стадия ET-002 и путь срабатывания задокументированы в ADR/12-review).
- Тест-план `04-test-plan.yaml` реализован в `tests/test_reconciler.py`.
- `pytest tests/ -q` зелёный.
