tasks: заведён остаток работы о гейте, контексте и токене

- четыре новые записи: проверить шаг migrations так же, как шаг сверки версий Go;
  свести шесть расхождений между документами канона; запретить обращаться к Bot
  API мимо клиента бота; разведка о шагах гейта, теряющих предмет
- context-cancel-in-pipeline приведена к правде: дописан перечень сделанного
  попутно, критерий с оракулом «тест на трёх прерываниях подряд» разбит надвое —
  проверена была только его узкая половина
This commit is contained in:
av
2026-08-13 10:55:38 +03:00
parent f4d8c7ed50
commit eacaf76d5f
6 changed files with 230 additions and 7 deletions
@@ -0,0 +1,50 @@
# 🧹 Запретить обращаться к Bot API мимо клиента бота
- **Тип:** chore
- **Категория:** Очередь — Класс уже дал утечку токена; сегодня его держат две проверки на сегодняшних местах, а не правило.
- **Зачем:** Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
Токен бота стоит в пути каждого обращения к Bot API, а `http.Client` кладёт
адрес запроса в `*url.Error` целиком. Поэтому 2026-08-13 чистка перенесена на
границу клиента: `internal/adapter/telegram`, `NewBot` — свой `Do` разворачивает
отказ в первопричину, а подменённый логгер вычищает токен из строк самой
библиотеки. Случай записан в журнал дефектов
[review.md](../../docs/review.md), 2026-08-13.
Держится это на двух стыках, и оба обходятся одной строкой: скачивание записи
берёт клиента у бота (`c.bot.Client.Do`), а не общий, и бот заводится единой
точкой, а не `tgbotapi.NewBotAPI`. Проверки на оба стыка есть
(`internal/controller/tg/download_test.go`,
`internal/adapter/telegram/bot_test.go`), но они судят **сегодняшние** места:
новый файл в транспорте, заведший свой клиент, они не увидят.
Ступень по лестнице механизации — четвёртая: свойство о структуре, а не о
вызове, и выражается тест-сканером в `internal/archrules`.
## Затрагивает
- `internal/archrules` — новое правило-сканер и его перечень предметов;
- `internal/controller/tg` — как поднадзорный пакет;
- `internal/adapter/telegram` — как единственное законное место, где клиент
заводится;
- [go-linters.md](../../docs/conventions/go-linters.md) — строка в перечне
механизированного и снятие соответствующей прозы.
## Критерии приёмки
- Свой `http.Client`, `http.DefaultClient` или `tgbotapi.NewBotAPI` в
`internal/controller/tg` роняет проверку и называет файл. Оракул — мутация:
внести каждую из трёх форм по очереди, все три краснеют.
- Законное употребление в `internal/adapter/telegram` проверку не роняет.
Оракул — тот же прогон на нетронутом дереве, зелёный.
- Правило не молчит, потеряв предмет: переименованный или исчезнувший пакет
роняет проверку, а не проходит обходом по пустому перечню. Оракул — мутация с
переименованием каталога.
- Правило записано строкой в `go-linters.md`, и проза, которую оно заменило,
оттуда снята. Оракул — чтение записи.
## Рамки
Боевым токеном не запускаться: сканер читает исходники, живого бота ему не
нужно. Внутренний логгер библиотеки правилом не выражается — его сторожит
проверка `TestLibraryLoggerIsActuallyInstalled`, и это остаётся так.
+31 -6
View File
@@ -2,7 +2,7 @@
- **Тип:** chore
- **Категория:** Очередь — Отмена контекста трогает те же шаги, что и таймауты: одна окрестность кода, два захода дороже одного.
- **Зачем:** Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5.
- **Зачем:** Половина сделана 2026-08-13 — контекст доходит до внешних вызовов, а прерванный шаг оставляет задачу на повтор и не тратит попытку, — но осталось то, ради чего задача заводилась: хранилище контекста не принимает ни одним методом, и бюджет мягкой остановки не замерен.
Долг назван в `docs/architecture.md` — раздел про capability `pipeline`, где
отмена контекста посреди шага перечислена среди неописанного. Ревью задачи
@@ -16,6 +16,28 @@
задачи держится 9,53 секунды (таймаут занятости хранилища — 10 секунд) и
возвращает отличимый отказ, а не «работы нет».
**Сделано попутно 2026-08-13** (коммит `f494dcb`, работа о линтерах и контексте
— задачей не бралась, поэтому карточка и осталась открытой):
- контекст проложен от воркера и обоих входов до внешних вызовов: `ffmpeg` и
`ffprobe` заводятся через `exec.CommandContext`, SpeechKit и Object Storage
принимают его доводом, скачивание записи идёт запросом с контекстом;
- прерванный шаг приговора не выносит: задача остаётся пригодной к повтору,
попытку не тратит и отправителю о несуществующем сбое не сообщает. Отмена
различается по `ctx.Err()`, а не по ошибке: убитый процесс отдаёт
«signal: killed», и `errors.Is` его с отменой не свяжет;
- воркер не считает остановку отказом и не пишет о ней владельцу; задача не
забирается вовсе, если нас уже остановили;
- принятие операции SpeechKit от отмены защищено своим пределом: потерянный
идентификатор заставил бы повтор оплатить ту же запись второй раз;
- проверки: `internal/service/shutdown_test.go`,
`TestShutdownIsNotAFailure` в `internal/controller/worker/worker_test.go`.
Из пяти критериев приёмки закрыты два — «оборванный шаг оставляет задачу
пригодной к повтору» и «прерванный шаг не тратит попытку». Открыты три: бюджет
остановки не замерен (хранилище контекста не принимает, и обрывать в нём нечего),
три отмены подряд ни один тест не гоняет, спека `pipeline` отмену не описывает.
## Затрагивает
- цикл воркера `internal/controller/worker`;
@@ -32,11 +54,14 @@
- Шаг, идущий дольше мягкого таймаута, обрывается по сигналу остановки. Оракул —
тест с шагом, спящим дольше таймаута: остановка укладывается в бюджет, а не
доживает до жёсткой.
- Оборванный шаг оставляет задачу пригодной к повтору. Оракул — тот же тест:
после остановки задача не захвачена и выдаётся следующему.
- Отмена не путается с отказом: прерванный шаг не тратит попытку и не уходит в
«мертва». Оракул — тест на трёх прерываниях подряд, счётчик попыток не растёт
до предела.
- Оборванный шаг оставляет задачу пригодной к повтору. Оракул — тест
`TestShutdownDuringConversionKeepsJobRetryable`: после остановки задача не
захвачена, стоит в прежнем состоянии и выдаётся следующему прогону.
- Прерванный шаг не тратит попытку. Оракул — тот же тест: счётчик попыток задачи
после отмены равен тому, что был до захвата.
- Три отмены подряд не уводят задачу в «мертва». Оракул — тест на трёх
прерываниях подряд: счётчик попыток не растёт до предела, состояние задачи не
меняется.
- Спека `pipeline` описывает отмену, и раздел `Purpose` её больше не числит
неописанной. Оракул — `openspec validate --strict`.
@@ -0,0 +1,62 @@
# 🧹 Свести шесть расхождений между документами канона
- **Тип:** chore
- **Категория:** Очередь — Находки одной сверки: чинится одним заходом, пока помнится, чем каждое место было найдено.
- **Зачем:** Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; четыре из них в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
Находки сверки документов агентами `doc-consistency` и `doc-code-drift`,
прогнанной 2026-08-13 вместе с работой о контексте и токене. К той работе
расхождения отношения не имеют — они старше, и потому не чинились тем же
коммитом.
Каждое место названо с домом факта, то есть с тем документом, который прав:
1. **Панель администратора против Authelia.** `architecture.md`, «Открытые
вопросы»: «Панель администратора Authelia не закрывает». `security.md`,
«Периметр»: адрес `/_/` закрывает Authelia на обратном прокси решением от
2026-08-11. Дом — `security.md`.
2. **Провайдер OIDC не назван внешней зависимостью.** Работает с 2026-08-12, у
него два таймаута в `database.md` и нормативная спека `access`, но в таблице
зависимостей `architecture.md` его нет, а языковая модель числится там
«пятой зависимостью». Дом — `architecture.md`, править его.
3. **«Таймаута нет ни у одного обращения наружу»** (`architecture.md`,
«Эксплуатация») против таймаутов обмена кода у провайдера в `database.md` и
требования спеки `access`. Верно узкое утверждение: таймаутов нет у Telegram,
Object Storage и SpeechKit. Дом — `database.md`.
4. **gin в `README.md`.** Веб-фреймворка нет: HTTP-поверхность — роутер
встроенной PocketBase, и `logging.md` прямо говорит, что вместе с gin ушёл и
`sloggin`. Дом стека — `CLAUDE.md`.
5. **Вид временной метки.** `conventions/database.md`: RFC 3339 с `T`, секундная
точность. `docs/database.md`: `2006-01-02 15:04:05.000Z`, и вид обязателен
побайтово — сравнение в SQLite строковое. Дом — `docs/database.md`;
конвенции нужна строка «*Расхождение:*».
6. **Дубли текста в `CLAUDE.md`** — подавления `hadolint` и настройка
`errcheck` пересказаны там дословно, хотя обе преамбулы договорились, что
дом перечня подавлений — `go-linters.md`.
## Затрагивает
- `docs/architecture.md` — «Открытые вопросы», таблица внешних зависимостей,
раздел «Эксплуатация»;
- `docs/conventions/database.md` — вид временной метки;
- `docs/security.md` и `docs/database.md` — как дома фактов, если правка
потребует уточнить формулировку;
- `README.md` — перечень технологий;
- `CLAUDE.md` — раздел «Гейт», два места с пересказом.
## Критерии приёмки
- Ни одно из шести мест не отвечает на свой вопрос двумя способами. Оракул —
повторный прогон `av-dev-docs:healthcheck`: перечисленные шесть находок не
возвращаются.
- Провайдер OIDC стоит в таблице внешних зависимостей со своими четырьмя
столбцами отказа, и счёт зависимостей в «Открытых вопросах» сходится с
таблицей. Оракул — чтение таблицы: число строк против числа в тексте.
- Дрейф раскладки и битые ссылки не заведены. Оракул — `task gate` зелёный
(шаг `docs`).
## Рамки
Правится текст документов, а не поведение сервиса: если расхождение окажется
дефектом кода, оно уезжает своей задачей, а не чинится здесь. Спеки
`openspec/specs/` не трогаем — у них свой порядок изменения.
+34
View File
@@ -0,0 +1,34 @@
# 🔬 Шаги гейта, у которых правило может потерять предмет
- **Тип:** research
- **Категория:** Очередь — Разведка о чужих скриптах: пока ответа нет, неизвестно даже, есть ли работа.
- **Зачем:** У шага migrations страж предмета есть, у шагов docs, tasks и openspec неизвестно: они зовут чужие скрипты из плагинов, и правило, потерявшее файлы, зеленело бы молча.
Класс известен и записан: правило, чей предмет исчез, обходит пустой перечень
ноль раз и проходит зелёным. В `internal/archrules` от этого стоит
`TestПакетыПравилСуществуют` — он падает, когда пакет из правила переименован. У
шага `migrations` страж завёлся 2026-08-13: пустой каталог шагов роняет шаг с
кодом 3.
Чего не знаем: ведут ли себя так же `docs.py check`, `tasks.py check` и
`openspec.py check`. Скрипты чужие — они живут в плагинах `av-dev-docs`,
`av-dev-tasks` и `av-dev-code`, и править их в этом репозитории нельзя. Отсюда и
тип записи: способ починки зависит от ответа. Найдётся страж внутри — делать
нечего; не найдётся — либо обёртка в `Taskfile.yml` со своей проверкой предмета,
либо разговор с владельцем плагина.
## Вопрос
Какие шаги гейта проходят зелёными, когда предмет их правила исчез, — и чем это
чинится, если сам скрипт править нельзя?
## Куда ляжет ответ
`docs/research/gate-steps-subject-guard.md` — записка с перечнем шагов, снятыми
исходами (по каждому: что сделали с предметом, каким кодом ответил шаг) и
рекомендацией. Исход разведки — задачи на те шаги, где страж нужен и возможен.
## Рамки
Скрипты плагинов не правим: они не в этом репозитории. Прогоны идут на временном
клоне репозитория, каталоги `docs/` и `tasks/` рабочего дерева не трогаем.
@@ -0,0 +1,48 @@
# 🧹 Проверить шаг гейта migrations так же, как шаг сверки версий Go
- **Тип:** chore
- **Категория:** Очередь — Шаг уже стоит в гейте и уже назван стражем critical-инварианта в двух документах — необеспеченное обещание дороже отсутствующего.
- **Зачем:** Шаг охраняет critical-инвариант «применённый шаг схемы не переписывается», но своих проверок не имеет: дрейф шаблона имени, переезд каталога или потеря grep в конвейере оставят его вечно зелёным, и это не заметит ничто.
Шаг заведён 2026-08-13 и проверен мутацией на восьми исходах вручную — правка
уехавшего шага в дереве и в коммите, удаление, переименование, новый шаг, правка
`migrations.go`, отсутствующий ключ в `docs/.docs.json`, каталог без шагов,
неразрешимая база диффа. Прогон был разовым: в дереве от него не осталось ничего.
Прецедент рядом. У шага сверки версий Go есть спека
[toolchain](../../openspec/specs/toolchain/spec.md) и 20 мутационно проверенных
сценариев в `scripts/check_go_version_test.go`; заведены они после дефекта
2026-08-12, когда зелёный шаг не проверял ничего и образ перестал собираться.
Долг назван строкой в
[go-linters.md](../../docs/conventions/go-linters.md), «Границы: где что живёт».
**Развилка, решаемая внутри задачи:** нормировать шаг спекой (второй capability
о проверке, как `toolchain`) либо ограничиться проверками без нормы. Первое
дороже и даёт построчную сверку сценариев; второе закрывает регрессию, но
оставляет норму в комментарии `Taskfile.yml`.
## Затрагивает
- шаг `migrations` в `Taskfile.yml` — его логика разбора `git diff`;
- ключ `migrations` в `docs/.docs.json` — из него шаг берёт каталог;
- каталог шагов схемы `internal/adapter/repo/pocketbase/migrations/` как предмет
правила;
- возможно — новая capability в `openspec/specs/` и файл проверок рядом с
`scripts/check_go_version_test.go`.
## Критерии приёмки
- Переписанный уехавший шаг схемы роняет проверку. Оракул — прогон сценария на
временном клоне репозитория: правка файла шага даёт код 1 и называет файл.
- Новый файл шага проверку не роняет, и правка `migrations.go` тоже: строка
`Register` нового шага прибавляется именно там. Оракул — те же два сценария.
- Каталог без единого файла шага и отсутствующий ключ в `docs/.docs.json` дают
код 3, а не тихий ноль. Оракул — два сценария на временном каталоге.
- Проверка сценариев идёт в гейте, а не руками. Оракул — `task gate` красный при
внесённом нарушении шаблона имени файла шага.
## Рамки
Боевой каталог данных и файлы шагов схемы не трогаем: сценарии гоняются на
временном клоне репозитория. Чужие скрипты проверок (`docs.py`, `tasks.py`,
`openspec.py`) — не наши, они в задаче `gate-steps-subject-guard`.