config: включение Telegram разведено с ключом доступа

- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет,
  файл без него негоден; bot_token стал только ключом доступа и при
  enabled = false не читается вовсе, а пустой при enabled = true роняет старт
- выключенный вход даёт подъём одним входом без единого обращения к Telegram и
  записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение
- отказ разбора файла настроек больше не пересказывает toml — её ParseError
  несёт в тексте разбираемое значение, и оборванная строка секретного ключа
  уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
This commit is contained in:
av
2026-08-13 21:46:14 +03:00
parent 903941f587
commit cd57b68215
27 changed files with 1536 additions and 123 deletions
@@ -0,0 +1,66 @@
# Намерение объявляется признаком, а не выводится из ключа доступа
- **Дата:** 2026-08-13
- **Источник:** openspec/changes/archive/2026-08-13-telegram-enabled-flag/design.md
## Решение
Вход Telegram включается отдельным признаком `telegram.enabled`, а `bot_token`
означает только доступ. Признак **обязателен**: умолчания у него нет, и файл
настроек без него негоден — сервис выходит с ошибкой настройки, назвав
недостающий ключ.
## Почему
Прежде пустой ключ доступа значил разом две вещи — «вход выключен намеренно» и
«ключа нет», — и сервис поднимался без бота в обоих случаях. Цена расхождения
падала на выкладку: файл настроек собирает Ansible, и потерянный при сборке ключ
выглядел для сервиса как решение владельца.
Умолчания у признака нет, и это **намеренный отказ от очевидного подхода**
булев ключ обычно заводят с умолчанием. Цитата из источника:
> умолчание — это угаданное намерение, а признак заводится ровно затем, чтобы
> намерение объявляли. Файл, где его забыли, одинаково плохо читается в обе
> стороны, и любое умолчание делает одну из двух ошибок тихой.
Отвергнуты оба умолчания. «Включён» — файл без признака работал бы «как-нибудь»,
и разница между объявленным и угаданным намерением исчезала бы ровно там, где её
завели. «Выключен» — первый же подъём после выкладки выключил бы бота молча, то
есть дал бы исход, против которого написано само требование.
Отсутствие ключа судит **разбор**, а не значение: `toml.MetaData.IsDefined`
отличает «не задан» от «задан ложным», тогда как нулевое значение `bool` у обоих
одинаковое. Форма поля с указателем отвергнута: указатель пережил бы проверку и
уехал к потребителям, где `nil` уже невозможен, но выглядит возможным.
Тем же решением закрыт разрез текста отказа при разборе файла настроек. Цитата
из источника:
> Пересказывать библиотеку нельзя: она собирает текст отказа из разбираемого
> куска файла, и оборванная строка секретного ключа уехала бы в журнал вместе со
> значением.
Норму держит инвариант «Секрет не покидает конфиг», а форму записи — конвенция
настроек. Спеки загрузку настроек не нормируют, и это назначено явно: загрузка
не принадлежит ни одной заведённой capability.
## Последствия
- `+` потерянный при сборке файла ключ доступа роняет старт вслух, а не оставляет
сервис работать в половину силы;
- `+` выключенный вход перестал быть поводом для предупреждения: решение
владельца сообщается записью «к сведению», а предупреждение осталось за тем,
чего владелец не выбирал, — недоступностью Telegram;
- `+` оборванная строка секретного ключа больше не уносит значение в журнал
контейнера;
- `` **порядок выкладки стал обязательным**: шаблон настроек обязан получить
признак раньше накатки образа, иначе сервис не поднимется вовсе. Правило живёт
в [architecture.md](../architecture.md), раздел «Эксплуатация», и задаётся там
по ключу, а не по файлу целиком;
- `` один путь молчаливой потери бота остался: признак, ошибочно собранный
как «выключен», отличим от решения владельца только записью журнала. Признак
поднятости входа тут не помощник — он равен нулю и при недоступности Telegram;
- `` отказ разбора файла настроек стал беднее на текст библиотеки: место и ключ
названы, а что именно в строке не так — нет. Плата принята ради инварианта,
помеченного необратимым.
@@ -56,3 +56,11 @@
выкладки), Telegram не отвергает — его отвергает разбор адреса, и такой случай
попадает в недоступность, а не в ошибку настройки. Заметен он записью журнала,
а не отказом старта.
*Уточнено 2026-08-13:* исходов сборки клиента, роняющих старт, стало два —
к ответу «такого бота нет» добавился пустой ключ доступа при включённом входе.
Решение это не меняет: пустой ключ ошибкой настройки и был, просто прежде он
выражал ещё и отказ от входа, а теперь отказ выражает признак `telegram.enabled`
и до сборки клиента не доходит вовсе. Недоступность Telegram по-прежнему подъёму
не мешает — ровно как решено здесь. Разведение двух значений — отдельная запись,
[ADR-2026-08-13-telegram-intent-declared-not-inferred](ADR-2026-08-13-telegram-intent-declared-not-inferred.md).
+1
View File
@@ -35,6 +35,7 @@
| Дата | Запись | Статус |
| --- | --- | --- |
| 2026-08-13 | [Намерение объявляется признаком, а не выводится из ключа доступа](ADR-2026-08-13-telegram-intent-declared-not-inferred.md) | |
| 2026-08-13 | [Недоступность Telegram подъёму сервиса не мешает](ADR-2026-08-13-telegram-outage-does-not-block-startup.md) | |
| 2026-08-12 | [Файл записи закрыт защищённым полем и отдаётся вошедшему по токену файла](ADR-2026-08-12-protected-file-behind-session.md) | |
| 2026-08-12 | [Сессия живёт семь суток и не продлевает саму себя](ADR-2026-08-12-session-without-refresh.md) | |