Files
orchestrator/docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md

18 KiB
Raw Permalink Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-091 architecture architect accepted 2026-06-09 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 — ложная картина при откате. Цикл рендера (:474505) выводит -строку для каждой стадии _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. Блок тоталов задачи (:388404) уже суммирует все прогоны — заниженной остаётся строка стадии.

Ключевая структурная сложность (флаг ТЗ §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:
    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-формулами, что блок тоталов задачи (:388404):
    • 💰 cost = Σ float(cost_usd or 0);
    • 🔢 in = Σ _input_total(usage) (= Σ(input+cache_read+cache_creation)), out = Σ int(output_tokens or 0); формат <in>↓/<out>↑ сохранён;
    • 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.