18 KiB
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 — ложная картина при откате. Цикл рендера (
: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, минимизирующие регресс-поверхность:
- Полнота карты — расширить
_STAGE_STATUS_LABELнедостающими ключами (deploy-staging,cancelled); заменить runtime-фолбэк с «To Analyse» на нейтральный (капитализированное имя стадии). Полнота гарантируется тестом, итерирующимSTAGE_TRANSITIONS.keys()(единый источник истины), а не дублирующим списком. - Отражение откатов — ввести позицию стадии в конвейере из порядка
STAGE_TRANSITIONSи гасить✅-строку для стадий ПОЗЖЕ текущей позиции. Нормализацияdeploy-staging → deployприменяется только к вычислению текущей позиции (для гейта подавления), логикаis_active_stage— без изменений (нулевой регресс активного рендера). - Суммирование метрик —
_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_runscfinished_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); формат<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_posnever-raise (unknown → «далёкое будущее» = старое поведение, ✅ не пере-подавляется). - − При
stage='deploy-staging'строка «Внедрение» может показать✅по завершённому staging-прогону (до prod-деплоя). Это сохранённое поведение (NFR-2), не регресс и не дефект по BRD; нормализация затрагивает только подавление, не активность. - Откат: изменение docs/code-only в одном модуле + тесты →
git revertPR. 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.