## Критерии приёмки Постановка пришла текстом и критериев не назвала. Ниже — **предложенные**; данными они становятся после ответа на чекпоинте. - В секции `[telegram]` файла настроек есть ключ `enabled`, и он один решает, поднимается ли вход. Ключ доступа второго значения не несёт. - Файл настроек с `enabled = false` даёт подъём одним входом, к Telegram не уходит ни одного обращения, а в журнале ровно одна запись уровня `INFO`. - Файл настроек с `enabled = true` и пустым `bot_token` роняет старт; сообщение называет имя ключа и не содержит его значения. - Файл настроек без ключа `enabled` негоден: загрузка кончается отказом, и сообщение называет недостающий ключ. Умолчания у признака нет. - Прежние правила при включённом входе сохранены: отказ Bot API роняет старт, недоступность Telegram даёт подъём с записью уровня `WARN`. - Признак поднятости входа Telegram выставляется во всех случаях, включая выключенный. - `task gate` зелёный. Ниже — рубрика ревью дизайна, теми же критериями. Пункты, целиком совпавшие с перечнем выше, не повторяются. - **Таблица режимов полна.** Для каждой комбинации «признак задан или нет × признак истинен или ложен × ключ доступа пуст, непуст или подсказка» назван ровно один исход из трёх: подъём с ботом, подъём без бота, отказ старта. - **Опечатка не выключает вход молча.** `enable`, `Enabled`, ключ в чужой секции, отсутствующая секция — каждый случай даёт отказ, а не тихий выбор режима по нулевому значению. - **Проверка целиком предшествует необратимому.** Приговор о настройках выносится до открытия порта, до применения шагов схемы и до создания каталогов. - **Выбранный режим наблюдаем, и наблюдаемость различает основания.** Из журнала и метрик видно и «работает ли вход сейчас», и «по какому основанию он не поднят»: выбор владельца, ошибка настройки, недоступность собеседника. - **Решение о режиме принимается один раз и в одном месте.** Порядок «умолчания → файл → приговор» зафиксирован; ни один потребитель не пересчитывает «поднят ли вход» из полей настроек самостоятельно. - **Виды отказа различимы по сообщению:** файла нет, файл не разбирается, ключ не задан, ключ задан негодно — по каждому видно, что чинить, и код выхода ненулевой. - **Выключенный вход не оставляет хвостов.** Клиент не заводится, сетевого обращения нет, остановка не ждёт несуществующего собеседника. - **Обратная совместимость файла названа в обе стороны** — что делает новый код со старым файлом и старый код с новым, вместе с порядком выкладки и условиями отката. - **Отсутствие умолчания объявлено там, где записана конвенция**, а не только комментарием в коде. - **Отказ разбора файла настроек не несёт содержимого файла.** Поломанная строка секретного ключа даёт отказ с номером строки и именем ключа, но без единой подстроки значения. Несовпадение типов при этом по-прежнему называет ключ и типы. ## 1. Настройки - [x] 1.1 Добавить поле `Enabled bool` с тегом `toml:"enabled"` в `config.TelegramConfig`; умолчания в `defaultConfig()` для него не заводить и объяснить это комментарием - [x] 1.2 Поднять `MetaData` из `toml.DecodeFile` в `LoadConfig` и отказывать в загрузке, когда ключ `telegram.enabled` в файле не задан; сообщение называет ключ. `MetaData` наружу из `LoadConfig` не отдавать - [x] 1.3 Написать `TelegramConfig.Validate()` по образцу `AuthConfig.Validate()`: при `Enabled` и пустом `BotToken` вернуть отказ с именем ключа `bot_token` и без его значения - [x] 1.4 Позвать `cfg.Telegram.Validate()` в `main.go` рядом с проверкой секции `[auth]`; отказ роняет процесс через `logger.Error` и `os.Exit(1)` - [x] 1.5 Проверить `TelegramConfig.Validate()` тестами: включён и ключ есть — ошибки нет; включён и ключ пуст — ошибка называет `bot_token` и не несёт значения; выключен и ключ пуст — ошибки нет - [x] 1.6 Проверить `LoadConfig` тестом на временном файле: секция `[telegram]` без ключа `enabled` даёт отказ с именем ключа; с ключом — загрузка проходит и значение доезжает обоими значениями ## 1а. Отказ разбора не несёт содержимого файла - [x] 1а.1 В `LoadConfig` перестать заворачивать отказ `toml.DecodeFile` через `%w`: разобрать его по семействам и собрать сообщение самому - [x] 1а.2 `toml.ParseError` (через `errors.As`) — взять путь, строку, столбец и последний ключ; поле `Message` в сообщение не брать - [x] 1а.3 Прочие отказы декодера — взять текст как есть: он собран из имён ключей и типов. Причину разреза записать комментарием, иначе следующая правка сведёт две ветки в одну - [x] 1а.4 Проверить тестом на временном файле: строка `bot_token` с оборванной кавычкой даёт отказ, в тексте которого нет ни одной подстроки значения, но есть номер строки и имя ключа - [x] 1а.5 Проверить тестом, что несовпадение типов (строка вместо числа) по-прежнему называет ключ и типы ## 2. Сборка входа - [x] 2.1 Сменить подпись `buildTelegram` на приём `config.TelegramConfig` целиком и поправить вызов в `main.go` - [x] 2.2 Вынести решение в `telegramFromConfig(cfg, newBot, logger)`, где `newBot` — параметр-функция сборки клиента; `buildTelegram` подставляет `telegram.NewBot`. Ветка выключенного входа стоит **до** вызова `newBot`: выставляет признак поднятости в ноль, пишет одну строку уровня `INFO` и возвращает заглушку отправителя - [x] 2.3 Свести в `telegramFromBot` ветку `telegram.ErrEmptyToken` с веткой отказа Bot API: оба исхода — ошибка настройки, старт роняется - [x] 2.4 Обновить комментарий-разрез над `telegramFromBot`: он описывает три исхода по прежнему разрезу ## 3. Проверки поведения - [x] 3.1 Заменить тест `TestTelegramFromBotOnEmptyTokenGivesAbsentSender` проверкой нового исхода: пустой ключ при включённом входе роняет старт, заглушка не подставляется - [x] 3.2 Написать тест на выключенный вход через `telegramFromConfig`: старт не падает, ядро получает заглушку, в журнале ровно одна запись уровня `INFO` - [x] 3.3 Проверить главное утверждение выключенного входа **счётчиком, а не исходом**: `telegramFromConfig` с `Enabled = false` и заполненным (заведомо ненастоящим) ключом зовёт подставную сборку **ноль раз**. Судить по «бот не заведён, отказа нет» нельзя: эти утверждения остаются верными и тогда, когда ветка встала после обращения, а обращение ушло в живой Telegram - [x] 3.4 Оставшиеся тесты `telegramFromBot` (отказ Bot API, недоступность, живой бот) прогнать без правок по существу ## 4. Настройки и документы - [x] 4.1 Добавить `enabled` в секцию `[telegram]` образца `config.dist.toml` со значением `false` и комментарием: зачем поле, что значит каждое значение, что ключ обязателен и умолчания у него нет - [x] 4.2 Переписать комментарий к `bot_token` в образце: он больше не отвечает за включение входа - [x] 4.3 Поправить запрет «Боевым токеном бота не запускаться» в `CLAUDE.md`: локальный прогон идёт с `enabled = false`, а не с пустым токеном - [x] 4.0 Записать в `docs/conventions/config.md`, раздел «Секреты», правило «отказ загрузки настроек не несёт содержимого файла» с причиной: текст отказа собирает чужая библиотека, и разбираемый кусок попадает в него целиком - [x] 4.4 Поправить `docs/conventions/config.md` в **двух** разделах: «Проверка и остановка на старте» описывает прежний разрез по пустоте токена, а «Структура в коде» утверждает, что новое поле требует правки обоих мест, включая `defaultConfig()`. Записать там форму обязательного поля без умолчания, иначе следующий такой ключ получит угаданное намерение обратно - [x] 4.5 Поправить `docs/architecture.md` в **двух** местах: строку перечня capability («незаданный вход Telegram не мешает подъёму» — после изменения ложно и ссылается на снятое имя требования) и строку про Telegram в таблице отказов - [x] 4.6 Поправить `docs/review.md`: рецепт живого прогона там велит поднимать сервис с пустым `telegram.bot_token`, а после изменения так он не встанет - [ ] 4.7 Завести ADR о том, что намерение объявляется признаком, а не выводится из ключа доступа, и проставить парный статус `ADR-2026-08-13-telegram-outage-does-not-block-startup`: он утверждает, что старт роняет ровно один исход сборки клиента, а после изменения их два. Заводить через скилл `av-dev:doc-sync`, на шаге синка документации ## 5. Гейт и живой прогон - [ ] 5.1 `task gate` зелёный — **не выполнено, и причина не в этой работе**: шаги `docs` и `tasks` красные оба по одной причине — проект приведён к раскладке av-dev версии 3, а плагин ждёт версии 4. Проверено на чистом `HEAD` в отдельном рабочем дереве: там те же два шага и то же расхождение. Чинится операцией `upgrade` скилла `av-dev:canon`, и это отдельная работа. Прочие шаги гейта зелёные - [x] 5.2 Живой прогон: подъём с `enabled = false` — сервис встаёт, в журнале одна запись `INFO`, признак поднятости входа Telegram равен нулю - [x] 5.3 Живой прогон: подъём с `enabled = true` и пустым `bot_token` — процесс выходит с ненулевым кодом, сообщение называет ключ - [x] 5.4 Живой прогон: подъём с секцией `[telegram]` без ключа `enabled` — процесс выходит с ненулевым кодом, сообщение называет недостающий ключ