From 53022d20f4b1ca8c4604e958905d0d0bc117e62b Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 21:46:55 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=505 --- docs/architecture/README.md | 2 +- docs/architecture/internals.md | 2 +- ...ADR-001-tracker-status-rollback-metrics.md | 195 ++++++++++++++++++ docs/work-items/ORCH-091/10-tech-risks.md | 37 ++++ 4 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md create mode 100644 docs/work-items/ORCH-091/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 546cf70..ddba666 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -15,7 +15,7 @@ - **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`. - **Disk-watchdog** (`src/disk_watchdog.py`, ORCH-063 — [adr-0024](adr/adr-0024-disk-watchdog.md)) — фоновый daemon-поток (каркас `reconciler`/`job_reaper`), стартует/останавливается в `main.lifespan` (старт последним — после `reaper.start()`; стоп первым в reverse-порядке; гард `disk_monitor_enabled`). Каждые `disk_monitor_interval_s` (дефолт 300с) меряет заполнение **хост-ФС** по смонтированным bind-путям (`/repos`, `/app/data`) через stdlib `shutil.disk_usage` (не overlay `/` контейнера, не субпроцесс `df`; дедуп путей по `st_dev`). Решение об алерте — pure-функция `decide_action(used_pct, threshold, prev_state, now, realert_s)`: алерт на пересечении порога (дефолт **85%**), cooldown-повтор `disk_monitor_realert_s` (анти-спам, не на каждом тике), однократный recovery при возврате ниже порога. Алерт — `send_telegram` (notifying, best-effort). Состояние анти-спама — in-memory (без миграции БД). never-raise (per-path/per-tick/per-send); только читает и уведомляет — не трогает диск/контейнер, не рестартит прод (self-hosting безопасность). Kill-switch `ORCH_DISK_MONITOR_ENABLED`; снимок — блок `disk_monitor` в `GET /queue` (`enabled`/`threshold_pct`/`interval_s`/`realert_s`/`paths`[`used_pct`/`free_gb`/`alerting`/`last_alert_at`]). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`. - **Build-cache-pruner** (`src/build_cache_pruner.py`, ORCH-062 — [adr-0025](adr/adr-0025-build-cache-pruner.md)) — фоновый daemon-поток (каркас `disk_watchdog`), стартует/останавливается в `main.lifespan` (старт последним — после `disk_watchdog.start()`; стоп первым в reverse; гард `build_cache_prune_enabled`). «Вторая половина» disk-watchdog: **watchdog сигналит — pruner убирает**. Каждые `build_cache_prune_interval_s` (дефолт 21600с = 6ч) выполняет **строго `docker builder prune -f --filter until=`** (BuildKit GC; дефолт `until=24h` — удаляет build cache старше суток, тёплый кэш сохраняет; `-a` опционально, только в паре с фильтром). Затрагивает **только** build cache — НЕ образы/контейнеры; рестарт docker daemon/прода не выполняется (self-hosting безопасность). В контейнере нет `docker` CLI (`Dockerfile:11`), поэтому уборка идёт **на хосте через ssh** каналом `deploy_ssh_user@deploy_ssh_host` (как `image_freshness`/`self_deploy`); пустой `deploy_ssh_host` → тик no-op (скоуп на self-host). never-raise (per-команда/per-tick); учёт результата in-memory (без миграции БД). Kill-switch `ORCH_BUILD_CACHE_PRUNE_ENABLED`; снимок — блок `build_cache_prune` в `GET /queue` (`enabled`/`interval_s`/`until`/`last_run_ts`/`last_reclaimed`/`last_error`). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`. -- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7 и [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md). +- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. **ORCH-091 (индикация-only):** три корректности рендера — (1) `_STAGE_STATUS_LABEL` покрывает ВСЕ ключи `STAGE_TRANSITIONS` (добавлены `deploy-staging`→«Deploying (staging)», `cancelled`→«Cancelled»; полнота гарантируется тестом по `stages.STAGE_TRANSITIONS`, не статичным списком — NFR-3), runtime-фолбэк для неизвестной стадии стал нейтральным (капитализированное имя) вместо «To Analyse»; (2) при откате конвейера `✅`-строки стадий ПОЗЖЕ текущей позиции (позиция — из порядка `STAGE_TRANSITIONS`, с нормализацией `deploy-staging→deploy` только в гейте подавления; `is_active_stage` не тронут) больше не рисуются; (3) строка стадии суммирует ВСЕ `agent_runs` агента (Σ cost/токены/время теми же формулами, что блок тоталов) → строгая сходимость с `SUM(agent_runs)`. Только `src/notifications.py` + тесты; `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/транспорт — не тронуты. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7, [ADR-087](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md) и [ORCH-091 ADR-001](../work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md). - **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту. - **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость). diff --git a/docs/architecture/internals.md b/docs/architecture/internals.md index 82980d7..75d52f6 100644 --- a/docs/architecture/internals.md +++ b/docs/architecture/internals.md @@ -135,7 +135,7 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash **Текст карточки (оба режима, ORCH-042):** метка `Подтверждение BRD` (была «Ревью БРД»); после прохождения approve-gate строка BRD начинается с ✅ (ветка ожидания сохраняет ⏸️/⏳); русские display-labels стадий (`Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение`); финальная строка `📦 Внедрено` (было `deployed`). Меняются только отображаемые строки — ключи стадий и имена агентов (завязаны на `_STAGE_ACTIVE_AGENT`, `last_done`, БД) не трогаются. **Строка Plane-статуса и кликабельный номер (ORCH-067, слой B — индикация).** Под заголовком карточка несёт строку `📍 ` по модели ORCH-066. Источник — двухслойный, контракт **never raises**: -- **Оффлайн-ядро** `plane_status_label(task_row)` — чистая функция БЕЗ сети: `stage → статус` (`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`, `development→Development`, `review→Code-Review`, `testing→Testing`, `deploy→⏸️ Awaiting Deploy`, `done→Done`) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). Неизвестная/битая стадия → безопасный дефолт `To Analyse`. +- **Оффлайн-ядро** `plane_status_label(task_row)` — чистая функция БЕЗ сети: `stage → статус` (`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`, `development→Development`, `review→Code-Review`, `testing→Testing`, `deploy-staging→Deploying (staging)` [ORCH-091], `deploy→⏸️ Awaiting Deploy`, `done→Done`, `cancelled→Cancelled` [ORCH-091]) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). **ORCH-091:** карта `_STAGE_STATUS_LABEL` покрывает ВСЕ ключи `STAGE_TRANSITIONS` (полнота — тестом, не статичным списком); неизвестная/будущая стадия → нейтральный фолбэк (капитализированное имя стадии), а НЕ «To Analyse» (он остаётся лишь явным лейблом `created` и безопасной деградацией на истинно-битом входе). - **Live-overlay** `_live_plane_branch_override` — best-effort: дорисовывает ветви-статусы, неразличимые оффлайн (Needs Input / Blocked / Rejected / Cancelled / Deploying / Monitoring after Deploy), чтением живого Plane-статуса (`fetch_issue_state` с коротким `tracker_live_status_timeout_s`, TTL-кэш `tracker_live_status_ttl_s`, kill-switch `tracker_live_status`). Любой сбой / выключенный флаг / нехватка данных → оффлайн-метка; `⏸️ In Review` (авторитет brd-часов) overlay не консультирует. Анти-false-positive: `deploying/monitoring`, алиасящие базовый UUID на проекте без выделенного статуса (enduro), не вызывают override. **Кликабельный номер задачи (ORCH-067).** Номер в заголовке карточки И во всех уведомлениях орка, где упоминается `work_item_id`, — HTML-ссылка на issue в Plane через общий `plane_issue_link` / `link_for` (URL строит `_plane_issue_url` с loopback/workspace/project-гардами, переиспользуя резолв ORCH-017). Fail-safe: при нехватке любого из (web-base/не-loopback, workspace, project_id, plane_issue_id) → `html.escape(work_item_id)` без ``; динамические части экранируются, ``-разметка валидна под `parse_mode=HTML`. Алерты `stage_engine`/`launcher`/`security_gate`/`reconciler` переведены на `link_for` (резолвит `repo`+`plane_issue_id` из БД по `task_id` или `work_item_id`). diff --git a/docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md b/docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md new file mode 100644 index 0000000..d0dcdc1 --- /dev/null +++ b/docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md @@ -0,0 +1,195 @@ +--- +work_item: ORCH-091 +stage: architecture +author_agent: architect +status: accepted +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# ADR-001: Карточка трекера — полнота карты статусов, отражение откатов, суммирование метрик по попыткам + +Work Item: **ORCH-091** — три верифицированных дефекта live-карточки (`src/notifications.py`) +Стадия: **architecture** +Сквозная регистрация: **N/A, локальное решение задачи** (затронут ровно один модуль +индикативного слоя `src/notifications.py`; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / +схема БД / транспорт нотификаций — не трогаются; новый компонент/стадия/гейт не вводятся). + +## Статус +Accepted + +## Контекст + +Live Telegram-карточка (ORCH-067/087, единственная карточка на задачу, +`render_task_tracker` / `plane_status_label`) — основной канал наблюдения за конвейером. +BRD/ТЗ объединяют три дефекта, сверенные по коду и БД прода (09.06): + +- **Деф.1 — застрявший заголовок «To Analyse».** `_STAGE_STATUS_LABEL` + (`src/notifications.py:940`) содержит 8 ключей (`created/analysis/architecture/development/ + review/testing/deploy/done`), а `tasks.stage` принимает ключи `STAGE_TRANSITIONS` + (`src/stages.py:12`) — среди них **`deploy-staging`** (не покрыт) и **`cancelled`** (ORCH-090, + не покрыт). `plane_status_label` (`:1009`) делает `.get(stage, _DEFAULT_STATUS_LABEL)` → + непокрытая стадия отдаёт дефолт **«To Analyse»** (`:950`). Из 10 реальных стадий не покрыты + две; дефолт-«To Analyse» — ещё и мина: любая новая стадия даст ложный «первый статус». +- **Деф.2 — ложная картина при откате.** Цикл рендера (`:474–505`) выводит `✅`-строку для + каждой стадии `_TRACKER_STAGES`, у чьего агента есть завершённый прогон (`last_done`), **без + учёта позиции стадии относительно текущей**. После отката (`deploy-staging → development`, + ORCH-43; `review → development`, REQUEST_CHANGES) карточка показывает «✅ Внедрение … + + 🔄 Разработка» — абсурд. +- **Деф.3 — занижение метрик строки стадии.** `_stage_line` берёт `run = last_done.get(agent)` + (`:475`) — ПОСЛЕДНИЙ прогон, теряя предыдущие попытки. Верифицировано на ORCH-069 (task 54): + developer 3 прогона Σ $3.98, карточка показывала ~$0.00. Блок тоталов задачи (`:388–404`) уже + суммирует все прогоны — заниженной остаётся **строка стадии**. + +Ключевая структурная сложность (флаг ТЗ §FR-4): `_TRACKER_STAGES` — 6 строк; стадии +`deploy-staging` и `deploy` **схлопнуты** в одну строку «Внедрение» (`stage_key="deploy"`, +агент `deployer`). `_STAGE_ACTIVE_AGENT` тоже не содержит `deploy-staging`. Любое решение по +порядку/позиции обязано не сломать этот сложившийся рендер строки «Внедрение». + +## Решение + +### Сводка + +Три аддитивные правки в `src/notifications.py`, минимизирующие регресс-поверхность: +1. **Полнота карты** — расширить `_STAGE_STATUS_LABEL` недостающими ключами + (`deploy-staging`, `cancelled`); заменить runtime-фолбэк с «To Analyse» на **нейтральный** + (капитализированное имя стадии). Полнота гарантируется **тестом**, итерирующим + `STAGE_TRANSITIONS.keys()` (единый источник истины), а не дублирующим списком. +2. **Отражение откатов** — ввести позицию стадии в конвейере из **порядка `STAGE_TRANSITIONS`** + и гасить `✅`-строку для стадий ПОЗЖЕ текущей позиции. Нормализация `deploy-staging → deploy` + применяется **только** к вычислению текущей позиции (для гейта подавления), логика + `is_active_stage` — **без изменений** (нулевой регресс активного рендера). +3. **Суммирование метрик** — `_stage_line` агрегирует ВСЕ `agent_runs` агента стадии + (теми же per-run-аккумуляторами, что и блок тоталов) → строгая сходимость с `SUM(agent_runs)`. + +Все функции остаются **stateless / never-raise**; любая ошибка деградирует к безопасному +выводу (старое поведение). + +### D1 — Полнота `_STAGE_STATUS_LABEL` + нейтральный фолбэк (Деф.1 / FR-1,2,3 / AC-1,2,3) + +- Расширить `_STAGE_STATUS_LABEL`, добавив **все** недостающие ключи `STAGE_TRANSITIONS`: + - `"deploy-staging": "Deploying (staging)"` — осмысленный staging-лейбл, согласованный с + моделью статусов ORCH-066/059: **plain-стиль** активной стадии (как `Analysis`/`Testing`, + без `⏸️`-маркера паузы), **отличен** от «To Analyse» и от лейбла `deploy` + («⏸️ Awaiting Deploy — ожидание Confirm Deploy»). Суффикс «(staging)» снимает коллизию с + prod-overlay «Deploying» (`_LIVE_BRANCH_LABELS['deploying']`). (FR-2 / AC-2.) + - `"cancelled": "Cancelled"` — offline-база для системного терминала ORCH-090. Совпадает с + overlay-лейблом `_LIVE_BRANCH_LABELS['cancelled']` ("Cancelled") → нет конфликта precedence + в `_card_status_label`; offline-путь больше не отдаёт «To Analyse» для отменённой задачи. +- **Runtime-фолбэк** в `plane_status_label`: вместо `_STAGE_STATUS_LABEL.get(stage, + _DEFAULT_STATUS_LABEL)` использовать **нейтральный** лейбл для отсутствующего ключа — + капитализированное имя стадии (напр. `stage.replace("-", " ").title()` → «Deploy Staging»), + с финальным безопасным дефолтом при пустом/битом входе. `created` сохраняет осмысленный + «To Analyse» как реальный первый статус (он **остаётся явным ключом** в карте). (FR-3 / AC-3.) +- `_DEFAULT_STATUS_LABEL` сохраняется как имя для `created` и для безопасной деградации на + истинно-битом входе (`None`/нет ключа `stage`); он **перестаёт** быть фолбэком для «известная + стадия, но нет лейбла» — этот путь теперь нейтрально-капитализированный. +- Спецветка `analysis` + открытый brd-clock → `_IN_REVIEW_LABEL` — **без изменений** (NFR-2). +- **Программная полнота (NFR-3)** обеспечивается тестом, который итерирует + `from src.stages import STAGE_TRANSITIONS` и для каждого ключа (кроме `created`) утверждает + непустой лейбл `≠ _DEFAULT_STATUS_LABEL`. Новая стадия без курируемого лейбла → красный тест. + **Запрещено** в самом модуле автогенерировать лейблы из имён стадий (теряется человеческая + осмысленность) — карта остаётся курируемой, тест лишь гарантирует её покрытие. + +### D2 — Отражение откатов: позиция из `STAGE_TRANSITIONS` (Деф.2 / FR-4 / AC-4) + +- Ввести в `src/notifications.py` (НЕ в `src/stages.py` — он read-only по ТЗ) лёгкий + индекс-хелпер от **единого источника порядка** `STAGE_TRANSITIONS`: + ```python + from .stages import STAGE_TRANSITIONS + _PIPELINE_ORDER = list(STAGE_TRANSITIONS.keys()) # created..done, cancelled + def _pipeline_pos(stage): # never-raise + try: + return _PIPELINE_ORDER.index(stage) + except (ValueError, TypeError): + return len(_PIPELINE_ORDER) # unknown -> «далёкое будущее» + ``` +- **Нормализация staging→deploy ТОЛЬКО для текущей позиции:** + `effective_stage = "deploy" if stage == "deploy-staging" else stage`; + `current_pos = _pipeline_pos(effective_stage)`. Это отражает, что строка «Внедрение» + представляет фазу deployer'а (staging+prod) как одну — иначе при `stage='deploy-staging'` + строка «Внедрение» (`stage_key="deploy"`, pos 7) была бы ошибочно подавлена (6 < 7). +- **Гейт подавления:** ветка `elif run is not None` (рендер `✅ <стадия>`) срабатывает **только + если** `current_pos >= _pipeline_pos(stage_key)`. Иначе (прогон есть, но стадия ПОЗЖЕ + текущей — откат) строка не выводится. Стадии ДО/НА текущей позиции сохраняют `✅` (фактически + пройденные). +- **`is_active_stage` — без изменений** (использует «сырой» `stage`, `_STAGE_ACTIVE_AGENT`, + `has_inflight`). Это даёт нулевой регресс активного/«just-finished snapshot» рендера (NFR-2): + при `stage='deploy-staging'` строка «Внедрение» ведёт себя как сегодня (✅ при завершённом + staging-прогоне, иначе ничего) — нормализация затрагивает лишь гейт подавления, не активность. +- Источник позиции — **порядок `STAGE_TRANSITIONS`**, а не индекс в `_TRACKER_STAGES` (NFR-3, + FR-4): добавление/перестановка стадий в движке автоматически корректирует подавление. +- **Условие 🔄 после отката (AC-4):** строка «Разработка» рисуется активной (`🔄`) существующей + логикой `is_active_stage`, которой нужен `has_inflight or run is None`. Реальное + пост-откатное состояние — reviewer/merge-gate ставит developer-job → launcher создаёт строку + `agent_runs` c `finished_at IS NULL` (in-flight) → `🔄`. Фикстура AC-4 обязана содержать + этот in-flight developer-прогон (так выглядит прод после отката). Наш фикс ортогонально + снимает ложные `✅` со стадий review/testing/Внедрение. + +### D3 — Суммирование метрик строки стадии (Деф.3 / FR-5 / AC-5) + +- `_stage_line` принимает **список прогонов** агента стадии (готовый + `agent_runs_by_agent.get(agent, [])`) вместо одного `run` и агрегирует **теми же + per-run-формулами, что блок тоталов задачи** (`:388–404`): + - 💰 `cost = Σ float(cost_usd or 0)`; + - 🔢 `in = Σ _input_total(usage)` (= Σ(input+cache_read+cache_creation)), + `out = Σ int(output_tokens or 0)`; формат `↓/↑` сохранён; + - ⏱ `dur = Σ _duration_seconds(started_at, finished_at)` (None-прогоны пропускаются, как в + тоталах). +- **Инвариант сходимости:** блок тоталов и строки стадий теперь аккумулируют **по одному и тому + же множеству строк `agent_runs`** и **одними формулами**; каждый агент привязан ровно к одной + строке `_TRACKER_STAGES` (analyst/architect/developer/reviewer/tester/deployer). Поэтому + Σ(показанных+подавленных строк стадий) ≡ тоталы задачи ≡ `SUM(agent_runs)` по `task_id` + (по стоимости/токенам/времени). Подавлённые откатом строки (D2) не рисуются, но их прогоны + **по-прежнему** входят в тоталы — это и есть намеренная семантика отката, инвариант AC-5 не + нарушается (тоталы считают всё; строка стадии — Σ своих прогонов). +- **Модель/эффорт/«попытка N» (ORCH-087, FR-5):** агрегируются метрики, но модель/эффорт + берутся из **последнего** прогона агента (`agent_runs` упорядочены `id ASC` → последний + элемент списка) через существующие `short_model_name` / `_run_effort`; счётчик попыток для + активной строки (`len(agent_runs)`) — без изменений. Формат строки байт-в-байт сохранён (NFR-2). + +## Альтернативы + +- **Автогенерация лейблов из имён стадий (полностью программная карта)** — отвергнуто: теряется + человеческая осмысленность («deploy-staging» → не лучше «Deploying (staging)»); курируемая + карта + тест полноты дают и читаемость, и анти-рассинхрон. +- **Нормализация `deploy-staging→deploy` во ВСЁМ цикле (включая `is_active_stage`)** — + отвергнуто как первичное решение: меняет активный рендер строки «Внедрение» на стадии + `deploy-staging` (риск регресса существующих тестов, NFR-2/AC-6). Нормализация ограничена + гейтом подавления — минимальная поверхность. +- **Позиция стадии из индекса `_TRACKER_STAGES`** — отвергнуто: `_TRACKER_STAGES` не содержит + `deploy-staging`/`cancelled` и не является источником истины о порядке конвейера (нарушает + NFR-3). Источник — `STAGE_TRANSITIONS`. +- **Изменение `_TRACKER_STAGES`/`_STAGE_ACTIVE_AGENT` (добавить deploy-staging-строку)** — + отвергнуто: вне объёма BRD (формат строк неизменен, NFR-2), расширяет регресс-поверхность. + +## Последствия + +- **+** Заголовок честен на всех стадиях (вкл. `deploy-staging`, `cancelled`); будущая стадия + не даёт ложный «To Analyse» (нейтральный фолбэк + тест полноты). +- **+** Карточка не «лжёт» после отката: `✅` снимается со стадий ПОЗЖЕ текущей позиции. +- **+** Метрики строки стадии = Σ всех попыток; строгая сходимость с `SUM(agent_runs)`. +- **+** Источник порядка/полноты — `STAGE_TRANSITIONS` (программно), анти-рассинхрон на будущее. +- **−** Новая read-only связь `notifications.py → stages.STAGE_TRANSITIONS` (порядок+ключи). + Митигейшн: импорт ключей, `stages.py` не изменяется (разрешено ТЗ §2); `_pipeline_pos` + never-raise (unknown → «далёкое будущее» = старое поведение, ✅ не пере-подавляется). +- **−** При `stage='deploy-staging'` строка «Внедрение» может показать `✅` по завершённому + staging-прогону (до prod-деплоя). Это **сохранённое** поведение (NFR-2), не регресс и не + дефект по BRD; нормализация затрагивает только подавление, не активность. +- **Откат:** изменение docs/code-only в одном модуле + тесты → `git revert` PR. Kill-switch не + требуется (нет нового поведения конвейера; рендер never-raise деградирует безопасно). + +## Ссылки +- BRD: `docs/work-items/ORCH-091/01-brd.md` +- TRZ: `docs/work-items/ORCH-091/02-trz.md` +- Acceptance: `docs/work-items/ORCH-091/03-acceptance-criteria.md` +- Tech-risks: `docs/work-items/ORCH-091/10-tech-risks.md` +- Сверено по коду: `src/notifications.py` (`_STAGE_STATUS_LABEL:940`, `_DEFAULT_STATUS_LABEL:950`, + `plane_status_label:990`, `render_task_tracker:333`, `_stage_line:445`, `_TRACKER_STAGES:233`, + `_STAGE_ACTIVE_AGENT:248`, totals `:388-404`), `src/stages.py::STAGE_TRANSITIONS:12`, + `src/usage.py::_input_total:348`. +- Инварианты, которые НЕЛЬЗЯ ломать (прочитаны перед правкой): ORCH-067/ORCH-087 + (`docs/work-items/ORCH-067|ORCH-087/06-adr/`) — single-card, never-raise, разделение + offline-ядра и live-overlay; ORCH-090 (`adr-0026`) — терминал `cancelled`. + + diff --git a/docs/work-items/ORCH-091/10-tech-risks.md b/docs/work-items/ORCH-091/10-tech-risks.md new file mode 100644 index 0000000..e9b3cb9 --- /dev/null +++ b/docs/work-items/ORCH-091/10-tech-risks.md @@ -0,0 +1,37 @@ +--- +work_item: ORCH-091 +stage: architecture +author_agent: architect +status: accepted +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-091 — Карточка трекера (статусы, откаты, метрики) + +Work Item: **ORCH-091** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | Регресс существующих меток/строк при правке цикла рендера (In Review, Awaiting Deploy, Done, эффорт-суффикс, формат строк/тоталов) | Сред. | Сред. | `is_active_stage` не трогаем; нормализация только в гейте подавления (D2); формат `_stage_line` байт-в-байт; зелёные `tests/test_tracker_*` + `test_telegram_tracker` (AC-6). | +| TR-2 | Рассинхрон карты статусов с `STAGE_TRANSITIONS` в будущем (новая стадия без лейбла) | Сред. | Низ. | Полнота — тест по `STAGE_TRANSITIONS.keys()` (NFR-3); нейтральный фолбэк вместо «To Analyse» (D1) → даже без лейбла не «лжёт». | +| TR-3 | Неверная точка отсчёта позиции стадии → неверное снятие/сохранение `✅` (особенно схлопывание `deploy-staging`/`deploy` в строку «Внедрение») | Сред. | Сред. | Позиция из порядка `STAGE_TRANSITIONS`; нормализация `deploy-staging→deploy` только для current-pos (D2); сценарные тесты отката `deploy-staging→development` и `review→development` (AC-4). | +| TR-4 | Расхождение метрик строки стадии с тоталами задачи (двойной/потерянный учёт) | Низ. | Сред. | Строка и тоталы используют ОДНИ формулы (`_input_total`/`_duration_seconds`/`cost_usd`) над ОДНИМ множеством `agent_runs`; тест сходимости Σ(строки) == `SUM(agent_runs)` по `task_id` (AC-5). | +| TR-5 | Исключение в `render_task_tracker`/`plane_status_label` блокирует индикацию | Низ. | Сред. | Контракт never-raise сохранён; `_pipeline_pos` never-raise (unknown → «далёкое будущее» = старое поведение); деградация к безопасному выводу (NFR-1/AC-3,7). | +| TR-6 | Новая import-связь `notifications.py → stages` вводит цикл импорта | Низ. | Низ. | `stages.py` — лист без обратных зависимостей на `notifications`; импорт ключей словаря, не функций; `stages.py` не изменяется (ТЗ §2). | +| TR-7 | Фикстура AC-4 без in-flight developer-прогона → строка «Разработка» не `🔄` | Низ. | Низ. | ADR D2 фиксирует: пост-откатный `🔄` требует строки `agent_runs` c `finished_at IS NULL`; тест-план обязан включать такой прогон (реальное прод-состояние после relaunch). | + +## Сводный вывод + +Доминирующий класс — **риски регресса индикативного слоя** (TR-1/TR-3) и **сходимости метрик** +(TR-4). Все смягчаются тестами и минимальной поверхностью правок (один модуль, без затрагивания +`STAGE_TRANSITIONS`/`QG_CHECKS`/схемы БД/транспорта). Эскалация `arch:major-change` **не нужна**: +изменение локально, обратимо `git revert`, never-raise, kill-switch не требуется. Возврат в анализ +**не требуется** — BRD/ТЗ полны и реализуемы без нарушения принципов. Остаточный риск для +прод-конвейера (self-hosting) — **низкий**: слой чисто индикативный, управляющий конвейер +(стадии/гейты/очередь) не затрагивается, рендер деградирует безопасно. +