Files
transcriber/CLAUDE.md
T
av 3925c637f3 Гейт ловит достижимые уязвимости в зависимостях
- шаг vulns гоняет govulncheck последним: ему одному нужна сеть, и он самый
  долгий; отсутствие инструмента даёт код окружения, а не пропуск
- закрыты обе достижимые находки — grpc до 1.82.1, aws-sdk-go-v2/service/s3 до
  1.97.3 с eventstream 1.7.8; прогон на реальных ключах Yandex не делался
- место шага названо в семантике гейта: он судит достижимость из кода, и
  недостижимая GO-2026-5932 в golang.org/x/crypto/openpgp его не роняет
2026-08-12 22:04:53 +03:00

202 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
Памятка для работы над transcriber. Перед задачей прочитай также
[docs/passport.md](docs/passport.md),
[docs/architecture.md](docs/architecture.md) и
[docs/conventions/](docs/conventions/README.md).
Проект ведём по-русски.
## Что это
Сервис расшифровки аудио в текст. Принимает запись двумя входами — Telegram-бот и
HTTP API, — конвертирует её `ffmpeg` в ogg, отдаёт на отложенное распознавание
Yandex SpeechKit и возвращает текст туда, откуда пришла запись. Состояние задач,
метаданные и сами файлы лежат во встроенной PocketBase, и она же даёт владельцу
панель администратора.
Чего **не** делает: сам речь не распознаёт и своих моделей не держит, текст
руками не правит и в форматы документов не экспортирует, учётных записей не
заводит, с живым потоком не работает и складом произвольных файлов не служит.
Записи и расшифровки хранит бессрочно: решением от 2026-08-11 сервис — архив.
Перечень выше — выжимка, границу домена целиком держит
[docs/passport.md](docs/passport.md).
## Стек
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`.
## Инварианты
Что нарушать нельзя.
- **Секрет не покидает конфиг.** Токен бота, ключ SpeechKit, пара ключей Object
Storage и секрет клиента OIDC не попадают в git, в лог, в ответ пользователю и
в колонку `error_text`. Нарушение необратимо: утёкший ключ отзывают и меняют
вручную во всех местах выкладки. **critical**
*Изъятие:* секрет клиента OIDC живёт ещё и в настройках коллекции
пользователей хранилища — туда его кладёт приведение настроек при каждом
подъёме, потому что применённый шаг схемы не переписывается и не пережил бы
ротации. Чтение файла базы равносильно чтению этого секрета; перечисленные
места запрета это не отменяет.
- **Содержимое записи остаётся приватным.** Текст расшифровки, имя файла
пользователя и его сообщение в лог не пишутся — только длина и
идентификаторы. Нарушение необратимо: строки уже уехали в журнал контейнера.
**critical**
*Изъятие:* расширение — хвост после последней точки — в журнал попадает
собственным полем: по нему прослеживается путь записи. Имени файла в журнале
нет вовсе (инвариант ниже). Изъятие узкое
и кончается журналом: наружу, меткой метрики, расширение выходит только
приведённым к перечню известных форматов. Границу держит спека `intake`,
цена — [adr/ADR-2026-08-11-known-format-label.md](docs/adr/ADR-2026-08-11-known-format-label.md),
остаток — [docs/security.md](docs/security.md).
- **Бот отвечает только тем, кто в белом списке.** Бот проверяет отправителя до
любой работы, включая скачивание файла. Нарушение обратимо правкой конфига, но
чужие записи к тому моменту уже обработаны за наши деньги. **critical**
- **Принятая запись не теряется молча.** Отказ на любом шаге либо оставляет
задачу пригодной к повтору, либо переводит её в `failed` и сообщает
пользователю. Молчаливый выход из шага без записи в лог и без смены состояния
запрещён. Обратимо повторной отправкой, но пользователь об этом не узнает.
**major**
- **`NoopJobError` — не ошибка.** Значение «задач в этом состоянии нет» не
логируется, не считается в метрику и не поднимает уровень. Нарушение даёт
запись раз в секунду на каждый воркер. **major**
- **Миграция, уехавшая на сервер, не переписывается.** Изменение — только новым
файлом шага. Необратимо: хранилище считает применённое по имени файла.
**critical**
- **Имя файла в хранилище задаёт сервис, а в журнал не идёт.** Умолчание
PocketBase строит имя из имени, данного отправителем, — оно не применяется.
Само имя — последняя часть ссылки `/api/files/...`, поэтому в журнал пишется
расширение, а не имя: строка журнала иначе стала бы бессрочным ключом к чужой
записи. **critical**
- **Колонки очереди правятся в четырёх местах** пакета хранилища —
`applyToRecord`, `recordToJob`, константа `acquireColumns` и структура
`acquiredRow` с её `toJob`, — плюс шаг схемы. Компилятор видит два из них.
Колонка, забытая в паре `acquireColumns`/`acquiredRow`, приезжает из захвата
нулевой, и первый же `Save` пишет этот ноль поверх сохранённого значения:
поле теряется **только у задачи, попавшей к воркеру**. **major**
- **Результат пишет только держатель захвата.** Шаг, чей захват за время работы
достался другому, завершается без записи и без ответа отправителю. Иначе два
воркера пишут в одну задачу по очереди, а отправитель получает два ответа.
**major**
## Команды
```bash
go build ./... # CGO не нужен
go test ./...
go vet ./...
gofmt -l .
golangci-lint run
go run . -c config.toml # флаг -c или --config, по умолчанию config.toml
task image # docker-образ; тег и раскладка — docs/architecture.md
task gate # весь набор проверок разом
```
Локальный запуск требует `ffmpeg` и `ffprobe` в `PATH` и своего `config.toml`
скопируй `config.dist.toml` и заполни; известные прорехи образца перечислены в
[docs/conventions/config.md](docs/conventions/config.md) строками
«*Расхождение:*».
## Гейт
- **Команда целиком:** `task gate`. База диффа — переменная `BASE`, по умолчанию
`origin/master`; переопределяется `task gate BASE=<rev>`.
- **Где логи шагов:** вывод команды, отдельного файла нет.
- **Что означает каждый исход:** ненулевой код любого шага роняет гейт. У
`docs.py check`, `tasks.py check`, `openspec.py check` и
`scripts/check-go-version.sh` словарь кодов общий: 0 сошлось, 1 дрейф,
2 ошибка употребления, 3 окружение (не корень проекта, каталог или файл не
найден), 4 внутренний сбой. Последний своего словаря не заводит намеренно:
четвёртый шаг с собственной семантикой сделал бы это утверждение неверным.
Тому же словарю следуют **обёртки шагов** в `Taskfile.yml`: недостающий скрипт
— отказ окружения, код 3. Наружу все эти коды приходят одним: сам `task` на
любой отказ шага выходит с 201, а код шага печатает строкой
(«exit status 3»), поэтому словарь читается по коду скрипта.
- **Что красит безусловно и почему:** отказ сборки, тестов, `go vet`,
неотформатированный файл, находка `golangci-lint`, расхождение объявленных
версий Go, дрейф раскладки документов,
дрейф каталога задач, форма `openspec/config.yaml`, достижимая из кода
уязвимость в зависимостях (`govulncheck`). Машина проверяет всё
перечисленное, и это не обсуждается. Шаг, чей скрипт не найден, краснеет с
именем недостающего плагина, а не пропускается молча.
- **Шагу `vulns` нужна сеть**, и он один такой: база уязвимостей живёт на
vuln.go.dev. Без сети шаг краснеет, а не пропускается молча; сам инструмент
ставится `go install golang.org/x/vuln/cmd/govulncheck@latest`. Судит он
достижимость из кода: находка в модуле, чей уязвимый символ мы не вызываем,
шаг не роняет. Такая сегодня одна — `GO-2026-5932` в
`golang.org/x/crypto/openpgp`, исправления у неё нет вовсе.
- **Чего в гейте намеренно нет и кто тогда обязан это гонять:**
- **сборка образа** — дорога, и отказ от неё сознательный. Дешёвая замена
стоит шагом сверки версий: он сравнивает строки и ловит расхождение, из-за
которого образ перестаёт собираться, но собираемости не проверяет. Собрать
образ по-прежнему может только человек — `task image`, и на подъёме версии
это обязательно;
- `shellcheck` — shell-скрипт в гейте один, `scripts/check-go-version.sh`; три
соседних шага это Python в плагинах, и линтер оболочки к ним неприменим.
Линтер не заведён, и цена ему одна строка шага плюс одно подавление ложного
`SC1007` на идиому `CDPATH= cd`. Его не проверяет ничто, а это единственный
исполняемый файл проекта, которого не видят ни `go vet`, ни `golangci-lint`;
- `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс
коммита. Полную историю никто не проверяет;
- согласованность документов между собой и с кодом — её судят агенты, зовёт
их скилл `av-dev-docs:healthcheck`, и звать его надо руками;
- покрытие изменённых строк не считается ничем.
**Гейт на `master` сегодня зелёный целиком, и объявленных долгов у него нет.**
Красный шаг означает поломку — свою или чужую, но поломку, а не наследство.
Списывать отказ на долг больше нельзя: списывать не на что.
Два прежних долга закрыты и здесь названы, чтобы отказ на их месте читался как
новый:
- `golangci-lint run` давал 4 замечания — два непроверенных `Close` и два
сравнения ошибок приведением типа. Закрыто задачей
`errors-as-instead-of-typecast` 2026-08-11; тогда же у `errcheck` включена
настройка `check-blank`, поэтому `_ = x.Close()` больше не снимает замечание:
отказ, который решено не проверять, объявляют в `exclude-functions` поимённо;
- `go test ./...` чинила задача `http-handler-tests-never-green`.
## Запреты
- **Боевой каталог данных не трогать.** `data/` на сервере целиком: под ним и
база (`data/data.db`), и записи живых людей
(`data/storage/<коллекция>/<запись>/`). Локальный каталог данных — свой, его
ронять и пересоздавать можно свободно.
- **Боевым токеном бота не запускаться.** Второй процесс с тем же токеном
перехватывает обновления у работающего, и пользователь теряет ответы.
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
подставляй `internal/adapter/recognizer/memory.go`.
- **Выкладку не запускать.** `inv pl -- transcriber` из `pet-project-server`
запускает человек.
- **`testdata` в проекте нет.** Тесты, которым нужен файл, создают его во
временном каталоге и убирают за собой.
- **Временное** — `t.TempDir()` в тестах, `/tmp` вне их. В `data/` временное не
писать: этот каталог смонтирован на сервере.
## Работа
- **Основная ветка:** `master`. Коммиты идут в неё напрямую, веток и PR нет.
- **Сообщение коммита** без трейлера `Co-Authored-By`.
- **Необратимое** (спрашивается у человека всегда): применённая миграция, формат
файла на диске и раскладка каталога данных, публичный контракт HTTP API, имя
ключа конфига, любое действие с боевыми данными и с Yandex Cloud, ротация
секрета.
- **Что считается сломанным** — новый красный шаг гейта, которого не было до
твоей правки. Такое чинится прежде любой другой работы. Два объявленных долга
из раздела «Гейт» сломанным состоянием **не** считаются, пока их не закрыли
задачами.
- **Ориентир по размеру порции:** не замерялся.
- **Что такое «сделана»:** `task gate` зелёный и критерии приёмки проверены
поимённо.
## Язык
- Документация, комментарии, сообщения коммитов — русский.
- Код и идентификаторы — английский.
- Текст, который видит пользователь Telegram, — русский.