# Ревью изменения `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` что-то у себя, из отчёта нельзя.