14 KiB
work_item, stage, author_agent, status, created_at, model_used
| work_item | stage | author_agent | status | created_at | model_used |
|---|---|---|---|---|---|
| ORCH-095 | analysis | analyst | ready-for-review | 2026-06-09 | claude-opus-4-8 |
01 — BRD (бизнес-требования): ORCH-095 — HTML-инъекция «<1м» в render_task_tracker застывает live-карточку
Work Item: ORCH-095 · Repo: orchestrator · Стадия: analysis
1. Бизнес-контекст и проблема
Live-трекер задачи (src/notifications.py::render_task_tracker) — основной канал
видимости конвейера для оператора. Слава узнаёт состояние каждой задачи по её единственной
карточке в Telegram (инвариант «одна карточка на задачу», ORCH-042/067/087). Если карточка
перестаёт обновляться — оператор слепнет: задача реально идёт/завершилась, а карточка врёт.
Установленный факт (воспроизведён детерминированно 09.06, сырой ответ Telegram).
Прямой вызов editMessageText для застрявшей карточки ORCH-093 (message_id 18854) вернул:
400 Bad Request: can't parse entities: Unsupported start tag "1м" at byte offset 500
В тексте карточки на позиции ~379 присутствует подстрока <1м · … — длительность стадии
«меньше одной минуты», которую _fmt_minutes (src/notifications.py:288-289) рендерит как
литерал <1м. Карточка отправляется с parse_mode=HTML (editMessageText,
notifications.py:175). Telegram трактует <1м как открывающий HTML-тег → парсинг падает
с 400 → edit_telegram возвращает EDIT_FAILED → update_task_tracker по ветке
EDIT_FAILED (notifications.py:733-739) делает return, не отправляя новую карточку
(защита от дублей, ORCH-087) → карточка застывает на стейте, где <1м впервые попал в текст.
Цепочка отказа (по коду):
_fmt_minutes(<60s) → "<1м" → интерполируется в HTML без экранирования → editMessageText
400 can't parse entities → edit_telegram → EDIT_FAILED → update_task_tracker ранний
return → карточка не обновляется до конца жизни задачи.
Почему проявляется не на каждой задаче. Баг ловится только когда хотя бы одна
длительность стадии < 1 мин (seconds < 60) и эта строка попадает в текст, который затем
редактируется. Карточки ORCH-090/091 редактировались успешно (на момент edit в их тексте
<1м не было); ORCH-093 — упала. Это объясняет «плавающую» природу симптома.
Корневой класс дефекта — шире одного <1м. Текст карточки собирается с parse_mode=HTML
из смеси (а) намеренной разметки-обёртки (<a href> номер задачи, <b>) и (б) подставляемых
данных. Намеренная разметка экранироваться не должна; данные — должны. Сейчас
экранирован только заголовок (esc_title, notifications.py:428) и href/label внутри
plane_issue_link. Прочие данные — длительности (_fmt_minutes), метрики токенов/стоимости
(fmt_tokens/fmt_cost), имя модели (short_model_name), статус-лейбл
(_card_status_label) — вставляются без html.escape. <1м — первый сработавший
экземпляр этого класса; задача закрывает класс, а не единичный символ.
2. Объём (scope)
В объёме
- Устранить HTML-инъекцию в
render_task_tracker: любые данные, попадающие в текст карточки сparse_mode=HTML, не должны ломать парсер Telegram (< > &в данных безопасны). - Привести формат «длительность < 1 мин» к HTML-безопасному виду (экранированный
<1мИЛИ переформулировка<1м→~0м/< 1 минс экранированием). - Сохранить работоспособность намеренной разметки карточки (
<a href>номер задачи, жирный/прочее форматирование) — экранируются только данные, не обёртка. - Восстановить обновления уже застрявших карточек (после фикса карточка возобновляет обновления или переотправляется свежей).
- Юнит-покрытие HTML-безопасности всех динамических полей; зелёный регресс
pytest tests/ -q; запись вCHANGELOG.md.
Вне объёма
- Изменение
STAGE_TRANSITIONS,QG_CHECKS,check_*, схемы БД — не трогаются (баг чисто в слое рендера уведомлений). - Изменение режима трекера (
bump/edit), логики леджера сирот (ORCH-087), статусной модели ORCH-066, транспортных примитивов (send_telegram/edit_telegram/delete_telegram) — кроме точечной HTML-безопасности самого текста. - Редизайн раскладки/состава карточки, новые метрики, перевод строк.
- Изменение машинных вердиктов / frontmatter-контракта.
3. Заинтересованные стороны
- Заказчик / репортёр: Слава (оператор) — обнаружил баг 09.06 (карточка ORCH-093 застряла, «по 91 уже нету»).
- Затронуты: все наблюдатели Telegram-трекера по всем проектам (self-hosting: общий прод-инстанс обслуживает и enduro-trails — карточки их задач так же уязвимы при стадии < 1 мин).
- Принимает результат: reviewer/tester конвейера ORCH; финальная приёмка — оператор (карточки снова обновляются в реальном времени).
4. Бизнес-требования (BR)
- BR-1 — Карточка трекера, в тексте которой есть стадия длительностью < 1 мин, должна
успешно редактироваться (
editMessageText→200, не400 can't parse entities). Источник отказа — литерал<1мот_fmt_minutes— устранён. (⇒ G1, G2) - BR-2 — Все динамические значения, вставляемые в текст карточки с
parse_mode=HTML(длительности, метрики токенов/стоимости, имя модели/эффорта, имена/лейблы стадий, статус-лейбл, заголовок задачи), HTML-безопасны: символы< > &в данных не интерпретируются Telegram как разметка. (⇒ G1) - BR-3 — Длительность «меньше минуты» рендерится так, чтобы не выглядеть открывающим
HTML-тегом: экранированный
<1мИЛИ переформулировка (~0м/< 1 мин) с экранированием. Видимое оператору значение остаётся осмысленным («меньше минуты»). (⇒ G2) - BR-4 — Регресс намеренной разметки: кликабельный номер задачи (
<a href>,plane_issue_link) и любое форматирование-обёртка (<b>и т.п.) продолжают рендериться и оставаться кликабельными/валидными — экранируются только подставляемые данные, не разметка. (⇒ G3) - BR-5 — Уже застрявшая карточка (класс ORCH-093) после деплоя фикса возобновляет
обновления: либо успешный
editMessageTextна следующем переходе стадии, либо переотправка свежей карточки. Конкретный механизм восстановления (текст снова валиден → edit проходит, ИЛИ классификацияcan't parse entitiesкак пересоздаваемой) — решение архитектора; бизнес-требование — карточка перестаёт быть «замёрзшей сиротой». (⇒ G... / AC-4)
5. Нефункциональные требования (NFR)
- NFR-1 (never-raise):
render_task_trackerи весь путь уведомлений сохраняют контракт «никогда не роняют конвейер» — любая ошибка рендера/экранирования деградирует к fallback-строке, не исключение. - NFR-2 (нулевая регрессия разметки): существующие зелёные тесты трекера
(
test_telegram_tracker.py,test_tracker_*,test_notifications_orphans.py,test_notify_issue_links.py) остаются зелёными; кликабельность номера и формат строк не деградируют визуально (кроме намеренной смены вида «<1м»). - NFR-3 (self-hosting): фикс — изменение только слоя рендера уведомлений; прод-контейнер
orchestratorне перезапускается в рамках стадий разработки; обязательна страховкаdeploy-stagingперед прод-деплоем. Машина стадий/гейты/схема БД не затрагиваются. - NFR-4 (совместимость): изменение обратносовместимо по данным/схеме; не требует миграций; применяется к новым рендерам сразу после деплоя.
6. Допущения и ограничения
- Карточка всегда отправляется с
parse_mode=HTML(send_telegram:58,edit_telegram:175) — это инвариант (ссылки/жирный требуют HTML); переход наparse_mode=None/MarkdownV2 не рассматривается (сломает намеренную разметку, шире объёма). fmt_tokens/fmt_costсейчас выдают только цифры/./k/M/$(HTML-безопасно), но требование BR-2 покрывает их defence-in-depth на случай будущих изменений формата.- Telegram-лимит 48ч: карточки старше 48ч физически неудаляемы/неперезаписываемы — для них восстановление недостижимо (known-limitation, унаследовано от ORCH-087); BR-5 относится к карточкам в пределах окна.
- Источник
<1м—_fmt_minutes(единственная функция, эмитящая литерал<); прочие данные лишь потенциально опасны. Точка(и) внесения экранирования — решение архитектора (централизовать в_fmt_minutes/на точке рендера/обёрткой-хелпером).
7. Критерии успеха
Карточка задачи со стадией < 1 мин успешно редактируется (нет 400 can't parse entities);
все динамические поля HTML-безопасны; намеренная разметка (ссылка-номер, форматирование)
рендерится и кликабельна; застрявшие карточки возобновляют обновления; never-raise сохранён;
pytest tests/ -q зелёный; CHANGELOG.md обновлён. Детальные PASS/FAIL — 03-acceptance-criteria.md.
8. Риски
- Двойное экранирование уже экранированных полей (
esc_title, href/label вplane_issue_link) →&lt;в выводе. Митигировать на стадии архитектуры (экранировать ровно один раз на источник данных). - Случайное экранирование разметки-обёртки (
<a>,<b>) → ссылки/жирный перестают работать (регресс BR-4). Чёткая граница «данные vs обёртка». - Изменение вида «<1м» меняет визуал карточки — согласовать формулировку с оператором (BR-3 допускает оба варианта).
- Детали/перечень —
10-tech-risks.md(заполняет архитектор).