- Клиент бота собирается один раз и достаётся отправителю и транспорту; разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание при сборке ограничено сроком — иначе молчащий Telegram вешал подъём. - Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой, уровень по причине — WARN для неподнятого входа, ERROR для неназванного адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count. - Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше обращения к клиенту, то есть мимо чистки на его границе.
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, расшифровка, заливка — не
проверяет никто.
Чего не принесёт ни один прогон:
- Решения проекта не сверялись —
docs/adr/процессный, прогон его не открывает. Расхождение с записанным решением ловитav-dev:doc-healthcheck. - Записанные наблюдения не использовались —
docs/research/тоже процессный. Всякое число этого отчёта снято на этом прогоне. - Поимённой сверки с руководствами по стилю Go не задавал ни один проход.
- Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.
Сработавшие потолки. Потолок триажа: 19 находок → 6. Срезано поимённо: дубли в тестах (близко к вкусовщине; починено попутно, тот же файл правился находкой 1); избыточность представлений факта «Telegram не поднят» — шесть вместо четырёх, одно сократимо, последствие не названо; откат бинаря — уехал в промоут; вырожденный ответ библиотеки — в гипотезы.
Отдельная находка о самом прогоне: проходы отдавали сводки пересказом, и
свои блоки «Coverage of this pass» с потолками до триажа дошли не все — узнать,
срезал ли code или adversary что-то у себя, из отчёта нельзя.