Files
transcriber/docs/adr/ADR-2026-08-13-telegram-intent-declared-not-inferred.md
T
av cd57b68215 config: включение Telegram разведено с ключом доступа
- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет,
  файл без него негоден; bot_token стал только ключом доступа и при
  enabled = false не читается вовсе, а пустой при enabled = true роняет старт
- выключенный вход даёт подъём одним входом без единого обращения к Telegram и
  записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение
- отказ разбора файла настроек больше не пересказывает toml — её ParseError
  несёт в тексте разбираемое значение, и оборванная строка секретного ключа
  уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
2026-08-13 21:46:14 +03:00

5.6 KiB
Raw Blame History

Намерение объявляется признаком, а не выводится из ключа доступа

  • Дата: 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, раздел «Эксплуатация», и задаётся там по ключу, а не по файлу целиком;
  • один путь молчаливой потери бота остался: признак, ошибочно собранный как «выключен», отличим от решения владельца только записью журнала. Признак поднятости входа тут не помощник — он равен нулю и при недоступности Telegram;
  • отказ разбора файла настроек стал беднее на текст библиотеки: место и ключ названы, а что именно в строке не так — нет. Плата принята ради инварианта, помеченного необратимым.