Files
transcriber/openspec/changes/archive/2026-08-13-start-without-telegram-token/review/triage.md
T
av b733a84d6a telegram: сервис поднимается без бота и работает одним входом
- Клиент бота собирается один раз и достаётся отправителю и транспорту;
  разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет
  старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание
  при сборке ограничено сроком — иначе молчащий Telegram вешал подъём.
- Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой,
  уровень по причине — WARN для неподнятого входа, ERROR для неназванного
  адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count.
- Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше
  обращения к клиенту, то есть мимо чистки на его границе.
2026-08-13 19:10:08 +03:00

14 KiB

Ревью изменения start-without-telegram-token — отчёт триажа

Прогон 2026-08-13. Отчёт сохранён оркестратором: агент триажа записывать .md не вправе.

Сводка

  • Режим: по графу; изменение не закоммичено, база диффа origin/master.
  • Метка: large — крупное × знакомое. Повторная разметка после правок дизайна: первая давала medium, исходя из того, что ядро не тронуто; правки ревью дизайна это допущение сняли.
  • Гейт: зелёный целиком, 13 шагов, включая -race, golangci-lint, govulncheck. Оракул снят проходом autotests, триаж гейт не перезапускал.
  • Находок на входе: 19 (specs 3, code 6, architecture 3, adversary 4, ops 3, autotests 0). Осталось: 6 в основном списке, 2 гипотезы, 2 кандидата в промоут; срезы названы поимённо.

План с исходом по каждой теме

тема дом глубина кто закрывает исход
requirements openspec/specs + дельты change разбор specs закрыта, 3 находки
autotests CLAUDE.md, «Гейт» autotests закрыта, 0 находок
conventions docs/conventions/ разбор code закрыта, 6 находок
architecture docs/architecture.md + passport.md доказательство architecture закрыта, 3 находки
security docs/security.md доказательство adversary закрыта, 4 находки
operations docs/architecture.md «Эксплуатация» + database.md доказательство ops закрыта, 3 находки

Тем без дома нет, тем без отчёта нет. Своих тем у проекта нет, basics не запускался — все темы ядра закрыты именными проходами. Побочное следствие: независимого второго голоса о заниженности метки на прогоне не было.

Блокирует мердж

1. Токен бота уезжает в журнал целиком при опечатке — critical

internal/adapter/telegram/bot.go возвращал отказ конструктора без чистки. Отказ рождается в http.NewRequest на разборе адреса — до обращения к клиенту, то есть мимо safeClient и WithoutURL. Токен с управляющим символом или неверной %-последовательностью печатался в журнал целиком.

Оракул: воспроизведено тремя проходами независимо и триажем отдельно. Нарушены инвариант CLAUDE.md «Секрет не покидает конфиг» (critical) и MUST дельта-спеки intake.

Исход: починено инлайн. Отказ конструктора пропущен через WithoutURL. Заодно закрыта дыра в собственной проверке: прежний тест судил запрет годным токеном, то есть случаем, который и так работал. Добавлен тест с токеном, ломающим разбор адреса.

2. Молчащий Telegram вешает старт навсегда — major

Клиент собран из &http.Client{} без срока ожидания, сборка стоит до подъёма сервера. При Telegram, отвечающем молчанием, процесс висит бесконечно: порт не слушается, /health не отвечает, воркеры не запущены, в журнале ни строки.

Поведение предсуществует изменению, но изменение записывает его нормой. Довод дизайна «различать нечем» проверяемо неверен: отказ Bot API приезжает типом *tgbotapi.Error с кодом, транспортный — нашим после чистки.

Исход: развилка владельцу. Цена дописана в таблицу отказов docs/architecture.md; выбор поведения — за владельцем.

Стоит исправить сейчас

3. Сервис без Telegram выглядит здоровым — major

/health отдаёт статические 200 ok и о входах не знает; серий transcriber_* на /metrics при неподнятом боте ноль; поля «доставлено» в схеме нет. Владелец узнаёт о потерянном входе только из журнала контейнера и только до ротации.

Исход: развилка владельцу. Попутно исправлено фактическое: обоснование нормы называло третьим последствием перезапись служебных полей завершённой задачи — такого не бывает, переход в терминальное состояние снимает захват, и повторная запись натыкается на «захват потерян». Довод сведён к двум последствиям.

4. Задача из Telegram, потерявшая чат, считается успешной — major

Было ERROR и отказ шага, стало WARN и успех. Тем самым снят самый громкий детектор класса, который CLAUDE.md называет самым коварным: колонка, выпавшая из пары acquireColumns/acquiredRow, обнуляет чат у задачи, попавшей к воркеру.

Исход: развилка владельцу — развести уровни по причине или оставить.

5. Разрез старта не держался ни одним тестом — minor

Инвертируй разрез — весь набор оставался зелёным.

Исход: починено инлайн. Решение вынесено из main в telegramFromBot и накрыто тремя случаями. Обращение к Telegram отделено от решения намеренно: обращение ходит в сеть и в проверке недоступно, а разрез проверять надо.

6. Образец конфига и три документа описывали снятое поведение — minor

config.dist.toml оставлял непустой плейсхолдер, хотя собственный комментарий рядом объявлял пустой токен режимом: копия образца старт роняла. docs/conventions/config.md описывал снятый механизм строкой «Расхождение», а она в этом проекте выдаёт индульгенцию будущим ревью. docs/conventions/errors.md перечислял удалённый тип. Маркер канона в docs/architecture.md ссылался на несуществующие capability.

Исход: починено инлайн, все четыре места.

Чем кончились развилки — решения владельца 2026-08-13

  • Находка 2 (молчащий Telegram). Выбран вариант сверх предложенных: недоступность Telegram на старт не влияет. Разрез перенесён с «пусто / непусто» на «ответил ли Telegram»: ответ «такого бота нет» роняет старт, недоступность даёт подъём без Telegram с записью WARN. Из решения следует срок ожидания при сборке — без него недоступность неотличима от подъёма.
  • Находка 3 (наблюдаемость). Выбран счётчик и признак входов: метрика поднятости по каждому входу и счётчик недоставленных ответов с причиной меткой. Колонку в задаче не заводили — это шаг схемы и необратимое.
  • Находка 4 (уровень). Уровни разведены: неподнятый вход — WARN, неназванный адресат — ERROR.

Отдельно о самом прогоне. Живой прогон, снятый проходом adversary, оставил процесс работающим на том же порту, и он держал его ещё час. Часть моих проверок после переделки мерила этот чужой процесс, а не новую сборку; обнаружено по отсутствию новой метрики, исправлено остановкой процесса и повторным прогоном. Кандидат в правило: прогон, поднимающий сервис, обязан снимать его за собой, а проверяющий — убеждаться, что порт занят его собственной сборкой.

Гипотезы без доказательства

  • {"ok":true,"result":null} считается успешной доставкой. Механизм доказан на подставном сервере, вторая половина — что живой Telegram так отвечает — не доказана и по правилам проекта недоказуема. Предсуществует изменению.
  • Второй os.Exit(1) недостижим сегодня. Приемлемая страховка, не дефект: конструктор объявляет отказ в сигнатуре, и разобрать его вызывающий обязан.

Кандидаты в промоут

  • Порядок выкладки: конфиг с пустым токеном нельзя выкатывать раньше бинаря. Воспроизведено на origin/master в отдельном worktree: откат бинаря при уже применённом пустом токене останавливает весь сервис. Это правило эксплуатации, которого в проекте нет; дом — docs/architecture.md, «Эксплуатация».
  • Сверка документов на упоминания удалённых идентификаторов. Три из четырёх мест находки 6 — прямые ссылки на снесённый код и несуществующие capability. Ловит это av-dev:doc-healthcheck, которого зовут руками.

Границы покрытия

Что не проверил ни один проход (docs/review.md, «Недоступно проверке»): поведение SpeechKit и Object Storage под нагрузкой; реальный профиль нагрузки; стойкость ffmpeg к вредоносному входу; поведение настоящей Authelia; поведение браузера с куками.

Перестали проверять сознательно: разбор вывода настоящего ffprobe; работа сервиса с настоящими внешними собеседниками. Подъём живьём стал доступен как раз этим изменением, но остаток — приём из Telegram, расшифровка, заливка — не проверяет никто.

Чего не принесёт ни один прогон:

  1. Решения проекта не сверялись — docs/adr/ процессный, прогон его не открывает. Расхождение с записанным решением ловит av-dev:doc-healthcheck.
  2. Записанные наблюдения не использовались — docs/research/ тоже процессный. Всякое число этого отчёта снято на этом прогоне.
  3. Поимённой сверки с руководствами по стилю Go не задавал ни один проход.
  4. Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.

Сработавшие потолки. Потолок триажа: 19 находок → 6. Срезано поимённо: дубли в тестах (близко к вкусовщине; починено попутно, тот же файл правился находкой 1); избыточность представлений факта «Telegram не поднят» — шесть вместо четырёх, одно сократимо, последствие не названо; откат бинаря — уехал в промоут; вырожденный ответ библиотеки — в гипотезы.

Отдельная находка о самом прогоне: проходы отдавали сводки пересказом, и свои блоки «Coverage of this pass» с потолками до триажа дошли не все — узнать, срезал ли code или adversary что-то у себя, из отчёта нельзя.