docs: правила о контексте и о токене записаны, две находки — в журнал

- в go-linters.md заведён раздел «Отмена и внешний собеседник», перечень
  механизированного пополнен девятью правилами и двумя шагами гейта, названы
  остатки: contextcheck не видит сигнатуру без контекста вовсе, шаг migrations
  судит только шаги, бывшие в базе диффа, rowserrcheck и sqlclosecheck
  профилактические — предмета в коде нет
- в logging.md и security.md чистка отказа Telegram описана по факту: точка одна
  и лежит на границе клиента, закрыты все пять путей вместе с логгером самой
  библиотеки. Прежнее «*Расхождение:* вычистки нет... она не логируется» было
  неверным дважды
- в журнал дефектов записаны две находки: отказ скачивания уносил токен бота
  (проскочил, жил с самого начала) и остановка сервиса хоронила конвертируемую
  запись в failed (поймано ревью до коммита)
- вопрос темы operations про отмену переформулирован: спрашивать надо не
  «доходит ли контекст», а «что шаг делает с задачей, деньгами и ответом
  отправителю»; вопрос про таймаут оставлен с оговоркой, что проброс контекста
  на него не отвечает
- в памятке: словарь кодов новых шагов, требование компилятора C у детектора
  гонок и оговорка, что «CGO не нужен» относится к сборке, а не к гейту
This commit is contained in:
av
2026-08-13 10:28:31 +03:00
parent bc5c35790e
commit bb9a67929c
6 changed files with 158 additions and 19 deletions
+10 -4
View File
@@ -24,7 +24,7 @@ Yandex SpeechKit и возвращает текст туда, откуда пр
## Стек
Go 1.26 (CGO не нужен), встроенная PocketBase — хранилище, файлы записей и
Go 1.26 (сборке CGO не нужен; детектору гонок в гейте — нужен), встроенная PocketBase — хранилище, файлы записей и
панель администратора, — `go-telegram-bot-api`, `aws-sdk-go-v2` для Object
Storage, gRPC-клиент Yandex SpeechKit v3, Prometheus, `slog`. Сборка —
Taskfile, образ — Docker, выкладка — Ansible из `pet-project-server`.
@@ -87,7 +87,7 @@ Taskfile, образ — Docker, выкладка — Ansible из `pet-project-
```bash
go build ./... # CGO не нужен
go test ./...
go test ./... # в гейте идёт с -race, и там нужен компилятор C
go vet ./...
gofmt -l .
golangci-lint run
@@ -117,13 +117,19 @@ task gate # весь набор проверок разом
найден), 4 внутренний сбой. Последний своего словаря не заводит намеренно:
четвёртый шаг с собственной семантикой сделал бы это утверждение неверным.
Тому же словарю следуют **обёртки шагов** в `Taskfile.yml` — все, включая
`shell`, `dockerfile` и `vulns`: недостающий инструмент — отказ окружения, код
3. Сами чужие инструменты (`shellcheck`, `hadolint`, `govulncheck`,
`tests`, `migrations`, `shell`, `dockerfile` и `vulns`: недостающий инструмент
— отказ окружения, код 3. У `tests` это отсутствие CGO или компилятора C, без
которых не работает детектор гонок — тесты он в этом случае всё равно гоняет,
без `-race`, и краснеет уже после них. У `migrations` код 3 — неразрешимая
база диффа, отсутствующий каталог шагов и каталог без единого шага; код 1 —
переписанный шаг схемы. Сами чужие инструменты (`shellcheck`, `hadolint`, `govulncheck`,
`golangci-lint`) держат свои коды, и гейту от них нужно только «ненулевой».
Недостающий скрипт — отказ окружения, код 3. Наружу все эти коды приходят одним: сам `task` на
любой отказ шага выходит с 201, а код шага печатает строкой
(«exit status 3»), поэтому словарь читается по коду скрипта.
- **Что красит безусловно и почему:** отказ сборки, тестов, `go vet`,
гонка, найденная детектором (`go test -race`), переписанный применённый шаг
схемы,
неотформатированный файл, находка `golangci-lint`, расхождение объявленных
версий Go, дрейф раскладки документов,
дрейф каталога задач, форма `openspec/config.yaml`, достижимая из кода
+7 -3
View File
@@ -93,8 +93,11 @@
## Внешние границы и форматы
- **Telegram Bot API.** Вход — обновления длинным опросом, выход — сообщения.
Файл скачивается по ссылке `file.Link(token)` обычным `http.Get`. Telegram не
отдаёт файлы больше 20 МиБ — это потолок приёма из бота.
Файл скачивается по ссылке `file.Link(token)` запросом с контекстом, клиентом
самого бота. Клиента заводит единая точка `internal/adapter/telegram`: токен
стоит в пути каждого обращения, и снятие адреса с отказа живёт там —
[conventions/logging.md](conventions/logging.md), «Безопасность: что не
логируем». Telegram не отдаёт файлы больше 20 МиБ — это потолок приёма из бота.
- **Yandex Object Storage.** S3-совместимый, клиент `aws-sdk-go-v2` с
`UsePathStyle`. Ключ объекта — имя файла, то есть UUID с расширением.
- **Yandex SpeechKit v3.** gRPC, `stt.api.cloud.yandex.net:443`, модель
@@ -118,8 +121,9 @@
| --- | --- | --- | --- | --- |
| Telegram Bot API | Бот не стартует, приложение продолжает работу без него | Скачивание файла висит бесконечно | Длинный опрос пуст, новые задачи не заводятся | Файл скачался битым, отказ вылезет на конвертации |
| Yandex SpeechKit | Шаг возвращает ошибку, задача остаётся на повтор | Захват держится час, задача не двигается | Операция вечно `in progress`, повтор каждые 5 секунд | Пустой текст — задача завершается заглушкой «на записи нет текста» |
| ↳ *остановка сервиса* | Принятие операции от отмены защищено своим пределом в 10 секунд: операцию там могли принять и начать считать деньги, а потерянный идентификатор заставил бы повтор оплатить ту же запись второй раз. Заливка в Object Storage отменяется штатно — её повтор бесплатен, объект ложится под тем же ключом | — | — | — |
| Yandex Object Storage | Заливка падает, задача остаётся в `converted` | То же, что падение: висит до конца захвата | — | SpeechKit не прочитает объект и вернёт отказ операции |
| ffmpeg, ffprobe | Задача уходит в `failed` с текстом «сбой конвертации файла» | Конвейер стоит: вызов синхронный | — | Выходной файл пуст, отказ вылезет на распознавании |
| ffmpeg, ffprobe | Задача уходит в `failed` с текстом «сбой конвертации файла». Остановка сервиса — исход другой: процесс убивают контекстом, и задача остаётся на повтор, не тратя попытки | Конвейер стоит: вызов синхронный | — | Выходной файл пуст, отказ вылезет на распознавании |
| Хранилище (файл на диске) | Приложение не стартует либо шаг падает на каждом запросе | Блокировка записи держит воркеры | — | — |
| Диск | Запись файла падает, задача не заводится | — | — | — |
+36 -1
View File
@@ -40,7 +40,11 @@ Go-проект как есть. Своё здесь — перечень пра
версий Go поведение нормировано отдельно, спекой
[toolchain](../../openspec/specs/toolchain/spec.md): это единственная проверка
проекта, у которой есть своя capability, и потому единственная, чьи сценарии
проверяются построчно (`scripts/check_go_version_test.go`).
проверяются построчно (`scripts/check_go_version_test.go`). Второй самодельный
шаг — `migrations` — нормы не имеет: он проверен мутацией на трёх исходах
(переписанный шаг, пустой каталог, чистое дерево), но регрессионных проверок у
него нет, и дрейф его собственного шаблона имени никто не поймает. Это
объявленный долг, а не умолчание.
## Лестница механизации
@@ -104,6 +108,9 @@ Go-проект как есть. Своё здесь — перечень пра
| Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml``errorlint` |
| Ошибка не узнаётся сравнением текста сообщения (`strings.Contains(err.Error(), …)`, `err.Error() == …`) | `internal/archrules``TestОшибкаНеУзнаётсяПоТексту` |
| Непроверенное возвращаемое значение ошибки | `.golangci.yml``errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close`, `os.Remove` и `send` |
| Непроверенное приведение типа (`v := x.(T)`) | `.golangci.yml``errcheck` с `check-type-assertions`. Отдельная настройка, потому что такое приведение паникует, а не возвращает ошибку, и `check-blank` его не видит |
| Проверенный отказ не оборачивается в `return nil` | `.golangci.yml``nilerr`. Механизирует половину инварианта «принятая запись не теряется молча»: молчаливый успех после отказа |
| Отказ выборки из хранилища не теряется (`rows.Err()`), а сама выборка закрывается | `.golangci.yml``rowserrcheck`, `sqlclosecheck`. **Профилактические: предмета в коде сегодня нет** — выборки идут через `dbx` хранилища, а из `database/sql` употребляются только `sql.NullString` и `sql.ErrNoRows`. Правила заведены на будущий сырой запрос; мутацией проверены на пробе, а не на своём коде |
| Ошибки — только stdlib, без сторонних пакетов | `.golangci.yml``depguard` |
### Структура и границы
@@ -115,6 +122,14 @@ Go-проект как есть. Своё здесь — перечень пра
| Адаптер не знает ни ядра, ни транспортов | `internal/archrules``TestАдаптерыНеЗнаютНиЯдра_НиТранспортов` |
| Колонки очереди согласованы: перечень захвата ↔ структура захвата ↔ шаг схемы ↔ запись коллекции ↔ перенос поля в задачу | `internal/archrules` → четыре правила о захвате. Закрывает инвариант «колонки правятся в четырёх местах» (CLAUDE.md, major), которого компилятор не держит. Литерал колонки ищется в телах нужных функций: по файлу целиком условие выполнялось бы тегами `db:"…"` самой структуры, и правило было бы зелёным всегда |
### Отмена и внешний собеседник
| Правило | Где механизировано |
| --- | --- |
| Запрос и внешний процесс заводятся с контекстом (`exec.CommandContext`, `http.NewRequestWithContext`, `QueryContext`) | `.golangci.yml``noctx`. Единая точка не нужна: контекст приезжает доводом, а контракты `internal/contract` несут его первым |
| Контекст приезжает сверху, а не заводится по месту (`context.Background()` в середине цепочки) | `.golangci.yml``contextcheck` |
| Тело ответа HTTP закрывается | `.golangci.yml``bodyclose`. Отдельно от `errcheck`: там `(io.ReadCloser).Close` объявлен исключением, и незакрытое тело от невыясненного `Close` неотличимо |
### Время, вывод, конфигурация
| Правило | Где механизировано |
@@ -130,6 +145,9 @@ Go-проект как есть. Своё здесь — перечень пра
| --- | --- |
| Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml``forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` |
| Каждый сценарий нормы шага сверки версий проверен мутацией, а не памятью | `scripts/check_go_version_test.go` — 20 сценариев спеки `toolchain` плюс два свойства самого шага: исход не зависит от установленного `go`, и шаг не зовёт ни `go`, ни `docker`, ни сеть |
| Форма утверждения в проверках: «ожидалось» и «получено» не перепутаны местами, отказ судится `NoError`, а не `Nil`, `require` не зовут из горутины | `.golangci.yml``testifylint` |
| Одновременный доступ проверен детектором, а не чтением кода | `Taskfile.yml` → шаг `tests` (`go test -race ./...`). Общее у воркеров — счётчики метрик, логгер и клиент бота; захват задачи в гонку не входит, он по построению её не даёт (одно состояние на воркер) — см. «Типовые ложноположительные» в [../review.md](../review.md). Без компилятора C шаг гоняет тесты без детектора и краснеет кодом 3: гонки — не повод отнимать у гейта сами тесты |
| Строчное подавление называет линтер и причину, а протухшее краснеет | `.golangci.yml``nolintlint` (`require-explanation`, `require-specific`, `allow-unused: false`) |
### Форма кода и файлов вне Go
@@ -146,6 +164,7 @@ Go-проект как есть. Своё здесь — перечень пра
| Правило | Где механизировано |
| --- | --- |
| Применённый шаг схемы не переписывается: у файла шага допустим один статус — `A` | `Taskfile.yml` → шаг `migrations`. Закрывает инвариант CLAUDE.md (critical), которого не держит ни компилятор, ни хранилище: применённое считается по имени файла. Баз диффа две — `BASE` и `HEAD`: первая отвечает на «шаг уже уехал» ровно настолько, насколько свежа `origin/master`, вторая ловит правку закоммиченного шага независимо от неё. Каталог берётся из ключа `migrations` в `docs/.docs.json`, чтобы у факта не было второго дома; пустой каталог роняет шаг — правило, потерявшее предмет, молчать не должно. `migrations.go` под правило не подпадает: строка `Register` нового шага прибавляется именно там |
| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` |
| Согласованность каталога задач, форма `openspec/config.yaml` | `tasks.py check`, `openspec.py check` |
| Секреты в коммите | `lefthook.yml``gitleaks git --staged` |
@@ -166,6 +185,7 @@ Go-проект как есть. Своё здесь — перечень пра
| Правило о заголовках вне `*_test.go` | `.golangci.yml`, `exclusions` | В рабочем коде `Header()` и есть способ отдать заголовок |
| `time.Now` внутри `internal/clock` | там же | Единой точке чтения времени нечем читать время иначе |
| Чтение времени и окружения в `*_test.go` | там же | Проверка строит вход прогона — фикстуру времени, `PATH`, окружение дочернего процесса, — а не метку домена и не настройки приложения. Исключение объявлено по тексту сообщения: правило называет четыре имени, и исключение обязано покрывать те же четыре |
| `noctx` на `httptest.NewRequest` в `*_test.go` | `.golangci.yml`, `exclusions` | Фикстура запроса к обработчику в том же процессе: внешнего собеседника за ней нет, отменять нечего. Изъятие названо по имени этой функции, а не выключением `noctx` на проверках: настоящий внешний вызов из проверки — `http.Get`, `exec.Command` — правилу по-прежнему подсуден, и это проверено мутацией |
| `SC1007` в `scripts/check-go-version.sh` | директива в скрипте | Ложное срабатывание на идиому `CDPATH= cd`, которая защищает `cd` от чужого `CDPATH` |
| `DL3007` (`alpine:latest`) | `Taskfile.yml`, шаг `dockerfile` | Открытая задача `pin-runtime-image-base`; до её решения шаг краснел бы на известном |
| `DL3018` (закрепить версии `apk`) | там же | Alpine не держит старые версии пакетов в репозитории: закрепление ломает сборку через недели |
@@ -185,6 +205,15 @@ Go-проект как есть. Своё здесь — перечень пра
- вывод в stdout через `fmt.Fprintln(os.Stdout, …)` и `os.Stdout.WriteString`:
`forbidigo` судит по имени вызванной функции, а не по её первому аргументу;
- проверка, судящая ответ мимо recorder — через свой `http.ResponseWriter`;
- **отсутствие** контекста у сигнатуры: `contextcheck` ловит обрыв цепочки —
`context.Background()` там, где контекст был доводом, — но метод, у которого
довода нет вовсе, правилу не виден. Первый проброс контекста в новый адаптер
остаётся человеку;
- шаг `migrations` судит только те шаги схемы, которые **есть в базе диффа**: у
добавленного после неё файла статус `A`, и правка такого файла законна — он
ещё никуда не уехал. Отсюда следствие: при отставшей `origin/master` правило
молчит на всём каталоге, и на подозрении база задаётся руками
(`task migrations BASE=<rev>`);
- направление «транспорт не знает адаптера»: сегодня оно нарушено осознанно —
`controller/http` импортирует адаптер хранилища, потому что HTTP-поверхность и
есть роутер этого хранилища. Изъятие названо в
@@ -223,5 +252,11 @@ Go-проект как есть. Своё здесь — перечень пра
принятое молчанием инструмента, — это не правило: прецеденты есть, и записаны
они в [../review.md](../review.md) (журнал 2026-08-11 про недостижимую норму,
2026-08-13 про обходимый текстовый запрет).
**Мутация ставится по одному нарушению на строку.** `golangci-lint` печатает
с одной строки исходника **одну** находку (умолчание `uniq-by-line`), и
мутация, задевшая сразу два правила, покажет только первое: так молчали
`sqlclosecheck` и `rowserrcheck` на пробе, где та же строка уже краснела от
`noctx`. Проверять правило пробой, где оно единственное нарушенное.
5. **Записать строкой здесь** и удалить прозу из конвенции, если правило её
заменило.
+16 -4
View File
@@ -241,10 +241,22 @@ Object Storage, скачивание файла из Telegram и опрос оп
проверка `errors.Is` на причину сохраняется. Общее правило: **секрет не кладём
в URL, если у сервиса есть заголовок** — тогда его нет и в ошибке транспорта.
*Расхождение:* вычистки нет. Скачивание файла из Telegram идёт обычным
`http.Get(file.Link(token))`, и ошибка этого вызова содержит токен бота. Сегодня
она не логируется — то есть утечки нет, но защищает от неё только отсутствие
строки лога.
Разговор с Telegram этому правилу следует, и точка чистки одна на все вызовы —
`internal/adapter/telegram`, `NewBot`. Токен стоит в пути **каждого** обращения к
Bot API, поэтому чистка на месте употребления закрывала бы один вызов из пяти:
- отказ транспорта разворачивает в первопричину клиент бота (`safeClient`), а
библиотека отдаёт наш отказ вызывающему нетронутым — этим закрыты `getFile`,
`sendMessage`, скачивание записи и `getMe` из конструктора;
- длинный опрос печатает свои отказы **пакетным логгером самой библиотеки**,
минуя наш `slog`; логгер подменён на вычищающий (`tgbotapi.SetLogger`), и
замена точная — токен известен.
Прежде здесь стоял `http.Get(file.Link(token))`, отказ уезжал в журнал вместе с
токеном, а конвенция числила это расхождением с оценкой «не логируется», которая
была неверной. Запись — [../review.md](../review.md), 2026-08-13; оракулом
служат проверки `internal/adapter/telegram/bot_test.go`, судящие по тексту
отказа и строке журнала.
*Изъятие, а не расхождение:* расширение берётся из имени отправителя дословно
(`filepath.Ext`), поэтому имя `запись.тайное-слово` отдаёт приватный хвост
+79 -5
View File
@@ -116,12 +116,18 @@
Форма: `<тема>: <вопрос> (<провенанс>)`.
- `operations`: пережил ли шаг конвейера отмену контекста на середине — воркеры
получают `ctx`, но ни один шаг его внутрь не передаёт (чтение `worker.go` и
`transcribe.go`, 2026-08-10).
- `operations`: как шаг отвечает на отмену посреди работы — контекст доходит до
внешнего собеседника и это держат правила `noctx` и `contextcheck`
([conventions/go-linters.md](conventions/go-linters.md), «Отмена и внешний
собеседник»), а исход прерванного шага нормой по-прежнему не описан
(`openspec/specs/pipeline`, `Purpose`). Спрашивать надо не «доходит ли», а «что
делает с задачей, деньгами и ответом отправителю» (чтение `worker.go` и
`transcribe.go`, 2026-08-13; прежний провенанс 2026-08-10 устарел вместе с
дефектом «остановка хоронила запись»).
- `operations`: появился ли таймаут у обращения к Telegram, S3 и SpeechKit — ни у
одного из них таймаута нет (чтение `tg.go`, `s3.go`, `speechkit.go`,
2026-08-10).
одного из них таймаута нет, и проброс контекста на этот вопрос **не отвечает**:
контекст здесь несёт жизнь процесса, а не дедлайн вызова (чтение `tg.go`,
`s3.go`, `speechkit.go`, 2026-08-13).
- `operations`: не удвоилась ли запись об одном сбое — шаг логирует ошибку и
возвращает её воркеру, который логирует снова (чтение `transcribe.go`,
2026-08-10).
@@ -240,6 +246,74 @@ API и имя не откатываются обратной правкой по
поймать их было некому. У восстановленных нет поля «Чем воспроизведён», и
выдумывать его задним числом нельзя.
## 2026-08-13 — остановка сервиса хоронила конвертируемую запись [пойман ревью]
- **Где:** `internal/service/transcribe.go`, шаг конвертации — дефект завела та
же правка, что проложила контекст до `ffmpeg`
- **Симптом:** на прод не уехал, поймали до коммита. Выглядел бы так: обычная
выкладка посреди конвертации переводит здоровую запись в терминальное
`failed`, отправителю уходит «сбой конвертации файла», а вернуть задачу может
только владелец правкой в панели. Окно — часы: конвертация шестичасовой записи
идёт дольше часа по построению
- **Причина:** контекст дошёл до внешнего процесса, а различать его отмену шаг
не научили. Убитый по контексту `ffmpeg` отдаёт `signal: killed` — от
настоящего отказа (`exit status N`) эта ошибка неотличима ни типом, ни
`errors.Is`: различает только `ctx.Err()`. Шаг звал `failJob` на любой отказ
`Convert`. Хуже: `failJob` возвращает `nil`, поэтому воркер считал прогон
успешным, и метрика владельца — та, которой он замечает отказы, — не
шевелилась
- **Чем воспроизведён:** проверкой `TestShutdownDuringConversionKeepsJobRetryable`
с подставным конвертером, ведущим себя как убитый процесс: отдаёт отказ, не
несущий `context.Canceled`. Мутация снята — без развилки проверка краснеет
- **Почему не поймали раньше:** правка выглядела механической, «линтер потребовал
контекст». Цена оказалась в семантике очереди, а не в сигнатурах: отмена стала
значить разное на соседних шагах одного конвейера. Ни один линтер такого не
видит — это заметили три прохода ревью независимо, и все три построили путь
- **Что меняем:** прерванный шаг приговора не выносит — задача остаётся на
повтор, попытку не тратит (счётчик, выросший при захвате, возвращают назад) и
отправителю о несуществующем сбое не сообщает. Воркер не считает остановку
отказом и не пишет о ней владельцу. Задача не забирается вовсе, если нас уже
остановили. Остаток объявлен: норма отмены в спеке `pipeline` не описана, и
открытая задача `context-cancel-in-pipeline` этим закрыта не целиком
## 2026-08-13 — отказ скачивания уносил токен бота в журнал [проскочил]
- **Где:** `internal/controller/tg/tg.go`, скачивание записи по ссылке
`file.Link(c.bot.Token)`
- **Симптом:** не наблюдался, потому что журнал за этим местом никто не читал
построчно. Первый же сбой сети на скачивании писал в журнал
`Failed to download audio file` вместе с полным адресом запроса, а в адресе
Telegram держит токен бота (`…/bot<TOKEN>/…`). Инвариант «секрет не покидает
конфиг» помечен critical и необратим: утёкший токен отзывают руками
- **Причина:** `http.Get` возвращает `*url.Error`, и тот встраивает адрес
целиком. Отказ уходил в `fmt.Errorf("failed to download file: %w", err)`, а
оттуда — в `logger.Error` соседней строкой
- **Чем воспроизведён:** чтением цепочки от `http.Get` до вызова `logger.Error`
в трёх обработчиках; на живом боте не проверялся — боевым токеном запускаться
запрещено
- **Почему не поймали раньше:** правило было записано прозой и ровно про этот
случай — [conventions/logging.md](conventions/logging.md), «Ошибка
HTTP-транспорта несёт URL». Хуже: там же стояло объявленное *Расхождение* с
оценкой «сегодня она не логируется — то есть утечки нет», и оценка была
неверной. Строка лога существовала всё это время, но проза о ней не знала, а
машина прозу не проверяет
- **Что меняем:** чистка перенесена с места употребления на **границу клиента**
`internal/adapter/telegram`, `NewBot`: свой `Do` разворачивает отказ в
первопричину, а подменённый логгер библиотеки вычищает токен из строк длинного
опроса, которые она печатает сама, мимо нашего `slog`. Транспорт бота токена
больше не получает: клиента ему отдают готовым. Расхождение в конвенции
закрыто, оценка в [security.md](security.md) исправлена
- **Чем закрыт от возврата:** проверками `internal/adapter/telegram/bot_test.go`
— четыре пути (`getFile`, `sendMessage`, конструктор, логгер библиотеки)
судятся по тексту отказа и строке журнала. Мутация снята: со снятой чисткой
три из них краснеют, печатая токен. Правило остаётся прозой (линтер не отличит
ссылку с секретом от ссылки без него), но у прозы теперь есть оракул
- **Как нашли:** первый путь — попутно, при разборе находок `noctx`: тот
потребовал переписать `http.Get` на запрос с контекстом, и цепочку пришлось
прочитать целиком. Остальные четыре — конвейером ревью в тот же день; правка,
закрывшая один путь, объявила класс закрытым в двух документах, и это едва не
осталось так
## 2026-08-13 — конец потока распознавания узнавался по тексту сообщения [пойман сканером]
- **Где:** `internal/adapter/recognizer/yandex/speechkit.go`, чтение потока
+10 -2
View File
@@ -267,8 +267,16 @@ Telegram отправителю.
приходит путь, выданный самим Telegram. Настоящее имя документа дальше проверки
типа файла не идёт.
Токен бота попадает в URL скачивания файла (`file.Link(token)`), и этот URL
нигде не логируется.
Токен бота стоит в пути **каждого** обращения к Bot API (`bot<TOKEN>/getFile`,
`…/sendMessage`, `…/getMe`, `…/getUpdates`) и в ссылке на скачивание
(`file.Link(token)`). Сами адреса нигде не логируются, но до 2026-08-13 их
уносил **отказ транспорта**: `*url.Error` встраивает адрес целиком, а отказы
скачивания и отправки пишутся в журнал. Теперь адрес снимается на границе
клиента — `internal/adapter/telegram`, `NewBot`: свой `Do` чистит отказ, а
подменённый логгер библиотеки вычищает токен из строк длинного опроса, которые
она печатает сама. Транспорт бота токена больше не получает вовсе: клиента ему
отдают готовым. Правило — [conventions/logging.md](conventions/logging.md),
случай — [review.md](review.md), оракул — `internal/adapter/telegram/bot_test.go`.
## Что вне модели