config: включение Telegram разведено с ключом доступа
- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет, файл без него негоден; bot_token стал только ключом доступа и при enabled = false не читается вовсе, а пустой при enabled = true роняет старт - выключенный вход даёт подъём одним входом без единого обращения к Telegram и записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение - отказ разбора файла настроек больше не пересказывает toml — её ParseError несёт в тексте разбираемое значение, и оборванная строка секретного ключа уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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) | |
|
||||
|
||||
+22
-8
@@ -17,7 +17,7 @@
|
||||
- [intake](../openspec/specs/intake/spec.md) — **приём по HTTP плюс наличие
|
||||
входов**: приём и опрос за сессией, имя отправителя не доходит ни до
|
||||
хранилища, ни до журнала, метка метрики несёт только известное расширение, а
|
||||
незаданный вход Telegram не мешает подъёму. Задачи
|
||||
выключенный вход Telegram не мешает подъёму. Задачи
|
||||
`http-handler-tests-never-green` и `no-user-filename-in-log` 2026-08-11,
|
||||
`pocketbase-storage` и `oidc-login` 2026-08-12,
|
||||
`local-run-without-telegram-token` 2026-08-13. Приём из Telegram по существу —
|
||||
@@ -114,12 +114,26 @@
|
||||
- **Где работает, что рядом, кто перезапускает:** один контейнер на личном
|
||||
сервере, разворачивает и перезапускает Ansible из `pet-project-server`. Рядом —
|
||||
обратный прокси, который публикует HTTP-порт наружу.
|
||||
- **Пустой токен бота нельзя разворачивать раньше образа, который его понимает.**
|
||||
Пустое значение стало объявленным режимом 2026-08-13; версии до неё роняли на
|
||||
нём старт с кодом 1 **до** открытия порта. Значит, откат образа при уже
|
||||
применённом пустом токене останавливает не бот, а весь сервис — вместе с HTTP
|
||||
и панелью. Порядок: сперва образ, потом конфиг; при откате — наоборот.
|
||||
Воспроизведено ревью кода на прежней версии.
|
||||
- **Порядок выкладки задаётся по ключу, а не по файлу целиком.** Общего правила
|
||||
«сперва образ» или «сперва конфиг» нет: два ключа секции Telegram требуют
|
||||
противоположного, и оба правила действуют одновременно.
|
||||
- **Признак включения `telegram.enabled` едет в конфиг раньше образа.** Он
|
||||
обязателен с 2026-08-13, умолчания у него нет, и образ, который его ждёт,
|
||||
без него выходит с кодом 1 **до** открытия порта — вместе с HTTP, панелью и
|
||||
конвейером. Прежний образ лишний ключ TOML просто не читает, поэтому ранняя
|
||||
правка конфига безопасна, а поздняя роняет сервис.
|
||||
- **Пустой ключ доступа `telegram.bot_token` едет позже образа.** Образы
|
||||
старше 2026-08-13 роняли старт на пустом ключе, тоже до открытия порта.
|
||||
- **Откат при выключенном входе** допустим только на образ от 2026-08-13 и
|
||||
новее. На более старом состояния «сервис поднят, бот опущен» не существует
|
||||
вовсе: пустой ключ роняет старт, негодный роняет старт, годный поднимает
|
||||
бота. Откат туда делают с непустым годным ключом, приняв, что бот поднимется.
|
||||
- **Откат образа при `enabled = false` и заполненном ключе** отменяет решение
|
||||
владельца молча: прежний образ признака не видит и поднимает бота. Если вход
|
||||
был выключен потому, что бот с этим токеном поднят где-то ещё, два процесса
|
||||
поделят один длинный опрос и часть ответов до людей не дойдёт.
|
||||
|
||||
Ревью кода воспроизвело порядок на прежней версии, живой прогон — на нынешней.
|
||||
- **Внешние зависимости поимённо и чем каждая отказывает.** Столбец «отвечает
|
||||
медленно» читается вместе с тем, что таймаута нет ни у одного обращения
|
||||
наружу — [database.md](database.md), «Настройки с числовым значением»:
|
||||
@@ -128,7 +142,7 @@
|
||||
|
||||
| Зависимость | Падает | Отвечает медленно | Молчит | Отдаёт мусор |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Telegram Bot API | Сервис поднимается без Telegram и работает по HTTP; старт роняет только ответ «такого бота нет». Норму держит [intake](../openspec/specs/intake/spec.md), «Недоступный или незаданный вход Telegram не мешает подъёму» | На старте — ждём не дольше срока, дальше поднимаемся без Telegram. У поднятого сервиса скачивание файла висит бесконечно: там срока нет | То же, что «отвечает медленно»: на старте — подъём без Telegram по истечении срока, у поднятого — длинный опрос пуст и новые задачи не заводятся | Файл скачался битым, отказ вылезет на конвертации |
|
||||
| Telegram Bot API | Сервис поднимается без Telegram и работает по HTTP; старт роняют только ошибки настройки — ответ «такого бота нет» и включённый вход с пустым ключом доступа. Норму держит [intake](../openspec/specs/intake/spec.md), «Признак включения решает, поднимается ли вход Telegram» | На старте — ждём не дольше срока, дальше поднимаемся без Telegram. У поднятого сервиса скачивание файла висит бесконечно: там срока нет | То же, что «отвечает медленно»: на старте — подъём без Telegram по истечении срока, у поднятого — длинный опрос пуст и новые задачи не заводятся | Файл скачался битым, отказ вылезет на конвертации |
|
||||
| Yandex SpeechKit | Шаг возвращает ошибку, задача остаётся на повтор | Захват держится час, задача не двигается | Операция вечно `in progress`, повтор каждые 5 секунд | Пустой текст — задача завершается заглушкой «на записи нет текста» |
|
||||
| ↳ *остановка сервиса* | Принятие операции от отмены защищено своим пределом в 10 секунд: операцию там могли принять и начать считать деньги, а потерянный идентификатор заставил бы повтор оплатить ту же запись второй раз. Заливка в Object Storage отменяется штатно — её повтор бесплатен, объект ложится под тем же ключом | — | — | — |
|
||||
| Yandex Object Storage | Заливка падает, задача остаётся в `converted` | То же, что падение: висит до конца захвата | — | SpeechKit не прочитает объект и вернёт отказ операции |
|
||||
|
||||
@@ -102,6 +102,17 @@ Ansible из `pet-project-server`). Приложение просто читае
|
||||
- Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво
|
||||
отрендеренный файл) — см. «Проверка и остановка на старте».
|
||||
- В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность».
|
||||
- **Отказ загрузки настроек не несёт содержимого файла.** Текст такого отказа
|
||||
собирает библиотека разбора, и собирает она его из разбираемого куска:
|
||||
`toml.ParseError` кладёт в сообщение само значение («Invalid float value: %q»).
|
||||
Оборванная кавычка в строке секретного ключа — типовая поломка криво
|
||||
отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а
|
||||
инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ
|
||||
разбора пересобирается своими словами: путь, строка, столбец и последний ключ,
|
||||
без сообщения библиотеки. Прочие отказы декодера (несовпадение типов,
|
||||
неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их
|
||||
текст остаётся как есть — иначе за разборчивость отказа платили бы там, где
|
||||
платить не за что.
|
||||
|
||||
## Проверка и остановка на старте
|
||||
|
||||
@@ -119,11 +130,16 @@ Ansible из `pet-project-server`). Приложение просто читае
|
||||
TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс
|
||||
выходит с кодом 1. Единого места проверки нет.
|
||||
|
||||
Токен бота под это расхождение больше не подпадает: он судится при сборке
|
||||
клиента, до подъёма сервера, и разрез у него объявленный — пустое значение
|
||||
означает отказ от входа и даёт подъём без Telegram, непустое негодное роняет
|
||||
старт как ошибка настройки. Нормирует это `openspec/specs/intake`, «Недоступный
|
||||
или незаданный вход Telegram не мешает подъёму».
|
||||
Под это расхождение больше не подпадают два ключа секции `[telegram]` — признак
|
||||
включения и ключ доступа, — и проверок у них две. Третий ключ секции,
|
||||
`update_timeout`, границ по-прежнему не проверяет никто, и ноль в нём обращает
|
||||
длинный опрос в непрерывный. Обязательность признака включения судит загрузчик — только разбор отличает
|
||||
«ключ не задан» от «ключ задан ложным», потому что нулевое значение `bool` у
|
||||
обоих одинаковое. Заполненность ключа доступа судит `TelegramConfig.Validate()` из
|
||||
`main.go`, рядом с проверкой `[auth]`: пустой `bot_token` при `enabled = true` —
|
||||
ошибка настройки и отказ старта. Непустой негодный по-прежнему судится при сборке
|
||||
клиента, до подъёма сервера. Нормирует это `openspec/specs/intake`, «Признак
|
||||
включения решает, поднимается ли вход Telegram».
|
||||
|
||||
Секция `[auth]` — первая, у которой проверка своя и стоит на старте:
|
||||
`AuthConfig.Validate()` зовётся из `main.go` сразу после загрузки и роняет
|
||||
@@ -142,3 +158,11 @@ TOML. Пустые ключи Yandex ловятся в конструкторе
|
||||
([ADR](../adr/ADR-2026-08-12-single-data-dir-config-key.md)).
|
||||
- Умолчания задаются в `defaultConfig()`, файл их перекрывает. Новое поле
|
||||
требует правки обоих мест.
|
||||
- **Обязательное поле — поле, у которого умолчания нет намеренно.** Умолчание у
|
||||
такого поля было бы угаданным намерением, и одна из двух ошибок стала бы
|
||||
тихой. Форма записи: умолчания нет ни в `defaultConfig()` (причина — строкой
|
||||
комментария у самого поля), ни по нулевому значению типа; присутствие ключа
|
||||
судит **разбор** — `MetaData.IsDefined` из `toml.DecodeFile`, — потому что
|
||||
значение отличить «не задано» от «задано нулём» не позволяет. В
|
||||
`config.dist.toml` у поля стоит значение свежей установки. Первое такое поле —
|
||||
`telegram.enabled`.
|
||||
|
||||
@@ -22,6 +22,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
||||
|
||||
| Дата | Запись | О чём |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-13 | [Разбор TOML: какое семейство отказов несёт значения из файла](toml-decode-errors.md) | Значения только в `ParseError.Message`, врущее поле `Line`, отказ значением в BurntSushi/toml v1.5.0 |
|
||||
| 2026-08-12 | [PocketBase: умолчания, которые ломают штатный сценарий](pocketbase-defaults.md) | Потолок файла 5 МиБ, тело 32 МиБ, таймаут чтения, суффикс имени, хук правки |
|
||||
| 2026-08-11 | [gRPC-клиент SpeechKit: когда закрытие вообще может отказать](grpc-client-close.md) | Ленивое соединение и два исхода `Close` в grpc v1.74.2 |
|
||||
| 2026-08-11 | [Фреймворк приложения: Svelte, Vue и React на одном экране](spa-framework.md) | Размер собранной статики, цена шага сборки, что у трёх кандидатов одинаково |
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Разбор TOML: какое семейство отказов несёт значения из файла
|
||||
|
||||
Отвечает на вопрос, возникший по ходу задачи `telegram-enabled-flag`: можно ли
|
||||
пересказывать отказ библиотеки разбора в журнал, если в файле настроек лежат
|
||||
секреты. Наблюдение понадобилось потому, что ревью дизайна назвало этот путь
|
||||
утечкой, а чинить его без разреза пришлось бы выбрасыванием всего текста отказа —
|
||||
то есть платой разборчивостью на каждой опечатке.
|
||||
|
||||
## Как снималось
|
||||
|
||||
Не замером, а **чтением исходников** зависимости, зафиксированной в `go.mod`:
|
||||
`github.com/BurntSushi/toml` версии **v1.5.0**. Смотрел `error.go`, `parse.go`,
|
||||
`decode.go`, `meta.go`, `lex.go` в кэше модулей. Дополнительно прогонял
|
||||
`toml.Decode` на правдоподобных опечатках — в каталоге вне репозитория, чтобы не
|
||||
править код проекта.
|
||||
|
||||
## Что выяснилось
|
||||
|
||||
- **Значения из файла несёт ровно одно семейство отказов — `toml.ParseError`.**
|
||||
Его поле `Message` собирается из разбираемого куска: `Invalid float value: %q`
|
||||
(`parse.go:341`), `invalid duration: %q`, `%v is out of range`, `Invalid
|
||||
integer %q`. Туда же лексер отдаёт свои отказы через `panicItemf`
|
||||
(`parse.go:134`).
|
||||
- **Прочие отказы декодера значений не содержат вовсе.** Их строит `md.e`
|
||||
(`decode.go:577`) и `md.badtype` — из имён ключей, имён типов (`%T` через
|
||||
`fmtType`) и длин. Обойдены все места: `decode.go:282,288,297,329,348,385,388,399,428,437,467,487,518,552,561`.
|
||||
- **`LastKey` секрета нести не может.** Текущий ключ присваивается только после
|
||||
`itemKeyEnd`, то есть после `=` (`parse.go:200`), а лексер ключа до `=` не
|
||||
доходит (`lex.go:481-501`). Посторонняя строка со значением ключом не станет.
|
||||
- **Поле `Line` у `ParseError` врёт, а `Position.Line` — нет.** `panicErr` и
|
||||
`panicItemf` кладут в устаревшее поле `Line` значение `it.pos.Len`, то есть
|
||||
**длину**, а не номер строки (`parse.go:97,106`). Брать надо `Position.Line`.
|
||||
- **`ParseError` возвращается значением, не указателем** (`decode.go:564`,
|
||||
`parse.go` целиком), поэтому `errors.As` берёт целью `toml.ParseError`, а не
|
||||
`*toml.ParseError`. `Unwrap` у типа нет.
|
||||
- **Ветка без последнего ключа достижима обычной опечаткой.** Незакрытая скобка
|
||||
секции даёт `LastKey=""`:
|
||||
|
||||
```
|
||||
вход "[telegram\nenabled = true\n"
|
||||
→ LastKey="" err=toml: line 2: expected '.' or ']' to end table name, but got '\n' instead
|
||||
```
|
||||
|
||||
## Что из этого следует для кода
|
||||
|
||||
Разрез по семейству отказа: `ParseError` пересобирается своими словами — путь,
|
||||
строка, столбец, последний ключ, — а его `Message` не берётся; прочие отказы
|
||||
проходят как есть. Так инвариант «Секрет не покидает конфиг» держится, а
|
||||
несовпадение типов по-прежнему называет ключ и типы.
|
||||
|
||||
**Наблюдение привязано к версии.** Версия, переложившая значение в другое
|
||||
семейство или сменившая возврат на указатель, вернёт утечку молча. Держат это
|
||||
проверки поломанного файла настроек в `internal/config/config_test.go`; при
|
||||
подъёме версии библиотеки их отказ читается как сигнал перечитать эту записку, а
|
||||
не как случайный шум.
|
||||
+47
-4
@@ -22,6 +22,15 @@
|
||||
новое поведение видно в выводе). Признак дешёвый: если ожидаемого нового поля,
|
||||
метрики или строки нет вовсе — вероятнее всего, отвечает не твой процесс.
|
||||
|
||||
**Ни один проход не сообщает свой потолок, и это надо читать как границу
|
||||
покрытия.** Прогон `telegram-enabled-flag` 2026-08-13: у прохода есть потолок
|
||||
находок, и устав велит объявлять строкой, сколько осталось за срезом и какого
|
||||
рода. Ни один из четырёх проходов такой строки не дал, и заметил это только
|
||||
триаж. Пока так, «находок больше нет» в отчёте прохода неотличимо от «больше не
|
||||
поместилось». Выше прочих риск у прохода, вбирающего темы разом: у него одна
|
||||
квота на три темы. Механизации нет — потолок объявляет сам проход, и заставить его нечем;
|
||||
остаётся сверка триажа.
|
||||
|
||||
Что уже проверяет машина и о чём поэтому спрашивать не нужно — конвенция
|
||||
[conventions/go-linters.md](conventions/go-linters.md). Вопросы ниже — то, чего
|
||||
машина не проверяет; свойства, которые обязан проверять тест, — в «Типовых
|
||||
@@ -240,11 +249,13 @@ API и имя не откатываются обратной правкой по
|
||||
`adapter/metaviewer/ffmpeg` нет; решение и его цена — в
|
||||
[adr/ADR-2026-08-11-stub-adapters-in-tests.md](adr/ADR-2026-08-11-stub-adapters-in-tests.md);
|
||||
- **работа сервиса с настоящими внешними собеседниками.** Сам сервис поднять
|
||||
теперь можно: с пустым `telegram.bot_token` он встаёт и работает одним входом
|
||||
(`openspec/specs/intake`, «Недоступный или незаданный вход Telegram не мешает
|
||||
подъёму»). Живой прогон — осмотр HTTP, панели, журнала и остановки — доступен
|
||||
теперь можно: с `telegram.enabled = false` он встаёт и работает одним входом
|
||||
(`openspec/specs/intake`, «Признак включения решает, поднимается ли вход
|
||||
Telegram»). Живой прогон — осмотр HTTP, панели, журнала и остановки — доступен
|
||||
теперь любой задаче. Прежняя формулировка «всё, что требует поднять сервис целиком»
|
||||
снята задачей `local-run-without-telegram-token` 2026-08-13.
|
||||
снята задачей `local-run-without-telegram-token` 2026-08-13; рецепт прогона
|
||||
сменился с пустого ключа доступа на выключенный вход задачей
|
||||
`telegram-enabled-flag` того же дня.
|
||||
|
||||
**Остаток**: за настоящий Telegram, SpeechKit и Object Storage живой прогон
|
||||
по-прежнему не отвечает — боевым токеном запускаться запрещено, ключи Yandex в
|
||||
@@ -260,6 +271,38 @@ API и имя не откатываются обратной правкой по
|
||||
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
|
||||
оракул, и выдумывать оракул задним числом нельзя.
|
||||
|
||||
## 2026-08-13 — сторож инварианта про секрет искал подстроку, которой не бывает [пойман ревью]
|
||||
|
||||
- **Где:** `internal/config/config_test.go`, проверка «значение ключа доступа не
|
||||
попадает в отказ» задачи `telegram-enabled-flag`. Дефект в самой проверке, кода
|
||||
сервиса он не касался
|
||||
- **Симптом:** проверка была зелёной и утверждала, что отказ `TelegramConfig.Validate()`
|
||||
не несёт значения ключа доступа. Приёмочный критерий задачи считался закрытым ею
|
||||
- **Причина:** двойная, и каждая половина достаточна. Утверждение искало
|
||||
подстроку `enabled = true при`, а в сообщении стоит `при enabled = true` —
|
||||
порядок слов обратный, и такой подстроки не бывает ни при каком входе. Глубже:
|
||||
`Validate()` отказывает **только** на пустом ключе, то есть значения, которым
|
||||
можно проговориться, на этом пути не существует вовсе. Комментарий при этом
|
||||
утверждал «Ключ непуст», а в теле стояло `BotToken: ""` — описан был не тот
|
||||
вход, который задан
|
||||
- **Чем воспроизведён:** триаж скопировал дерево во временный каталог и заменил
|
||||
тело `Validate()` на утекающее — `fmt.Errorf("... bot_token=%q ...", c.BotToken)`.
|
||||
Проверка осталась зелёной
|
||||
- **Почему не поймали раньше:** проверка написана в той же задаче и той же рукой,
|
||||
что и код; гейт зелёный, а зелёная проверка неотличима от работающей. Поймали
|
||||
два прохода независимо — разбор кода и сверка требований
|
||||
- **Что меняем:** проверка переписана честно и переименована: половина требования
|
||||
«сообщение не несёт значения» на этом пути **вакуумна**, и это названо прямо, а
|
||||
настоящий сторож той же нормы указан по имени — он живёт там, где непустой ключ
|
||||
в отказ попасть действительно может, в проверках отказа разбора файла настроек.
|
||||
Класс всплывает **третий раз** (2026-08-11 «проверка приёма не могла упасть»,
|
||||
2026-08-12 «проверка не могла упасть: читала живую карту заголовков»), и в этот
|
||||
раз он другой природы: прежние два ловились правилом линтера про источник
|
||||
утверждения, а этот — про **вход**: у сторожа утечки вход обязан содержать
|
||||
значение, которое может утечь, иначе сторож пуст независимо от формы
|
||||
утверждения. Механизации у этого нет и, похоже, быть не может: «может ли здесь
|
||||
вообще утечь» — суждение, а не форма. Остаётся проходу ревью
|
||||
|
||||
## 2026-08-13 — остановка сервиса хоронила конвертируемую запись [пойман ревью]
|
||||
|
||||
- **Где:** `internal/service/transcribe.go`, шаг конвертации — дефект завела та
|
||||
|
||||
@@ -288,6 +288,20 @@ Telegram отправителю.
|
||||
конструктора теперь чистится отдельно. Нашло это ревью кода тремя проходами
|
||||
независимо; оракул — там же, в `bot_test.go`.
|
||||
|
||||
Третий путь закрыт задачей `telegram-enabled-flag` 2026-08-13, и он **шире
|
||||
токена бота**: до неё утечь мог любой секрет конфига. Отказ разбора файла
|
||||
настроек пересказывался как есть, а библиотека разбора собирает текст отказа из
|
||||
разбираемого куска — `toml.ParseError` кладёт в сообщение само значение. Строка
|
||||
секретного ключа с оборванной кавычкой — типовая поломка криво собранного
|
||||
шаблона выкладки — уносила ключ в журнал контейнера целиком. Теперь такой отказ
|
||||
пересобирается своими словами: путь, строка, столбец и последний ключ, без текста
|
||||
библиотеки; прочие отказы декодера собраны из имён ключей и типов и потому
|
||||
проходят как есть. Нашло это ревью дизайна, чинилось решением владельца в той же
|
||||
работе. Правило — [conventions/config.md](conventions/config.md), «Секреты»;
|
||||
оракулы — `internal/config/config_test.go`, проверки поломанного файла настроек.
|
||||
Остаточный риск назван там же: разрез опирается на то, какое семейство отказов
|
||||
несёт значения **в нынешней версии** библиотеки.
|
||||
|
||||
## Что вне модели
|
||||
|
||||
Перечислить явно.
|
||||
|
||||
Reference in New Issue
Block a user