- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не приведением типа: обёртка `%w` на пути больше не превращает пустой прогон воркера в отказ раз в секунду - отказ закрытия соединения с распознавателем доходит до вызывающего (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе критерий принимал реализацию, выбрасывающую отказ в пустоту - заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх замечаний линтера закрыт, гейт зелёный целиком
12 KiB
CLAUDE.md
Памятка для работы над transcriber. Перед задачей прочитай также docs/passport.md, docs/architecture.md и docs/conventions/.
Проект ведём по-русски.
Что это
Сервис расшифровки аудио в текст. Принимает запись двумя входами — Telegram-бот и
HTTP API, — конвертирует её ffmpeg в ogg, отдаёт на отложенное распознавание
Yandex SpeechKit и возвращает текст туда, откуда пришла запись. Состояние задач
и метаданные файлов лежат в SQLite, файлы — на диске.
Чего не делает: сам речь не распознаёт и своих моделей не держит, текст руками не правит и в форматы документов не экспортирует, учётных записей не заводит, с живым потоком не работает и складом произвольных файлов не служит. Записи и расшифровки хранит бессрочно: решением от 2026-08-11 сервис — архив. Перечень выше — выжимка, границу домена целиком держит docs/passport.md.
Стек
Go 1.24 (нужен CGO из-за mattn/go-sqlite3), gin, goqu, goose, SQLite,
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 не попадают в git, в лог, в ответ пользователю и в колонку
error_text. Нарушение необратимо: утёкший ключ отзывают и меняют вручную во всех местах выкладки. critical - Содержимое записи остаётся приватным. Текст расшифровки, имя файла
пользователя и его сообщение в лог не пишутся — только длина и
идентификаторы. Нарушение необратимо: строки уже уехали в журнал контейнера.
critical
Изъятие: расширение — хвост после последней точки — в журнал попадает, потому
что стоит в собственном имени файла на диске, а путь логируется. Изъятие узкое
и кончается журналом: наружу, меткой метрики, расширение выходит только
приведённым к перечню известных форматов. Границу держит спека
intake, цена — adr/ADR-2026-08-11-known-format-label.md, остаток — docs/security.md. - Бот отвечает только тем, кто в белом списке. Бот проверяет отправителя до любой работы, включая скачивание файла. Нарушение обратимо правкой конфига, но чужие записи к тому моменту уже обработаны за наши деньги. critical
- Принятая запись не теряется молча. Отказ на любом шаге либо оставляет
задачу пригодной к повтору, либо переводит её в
failedи сообщает пользователю. Молчаливый выход из шага без записи в лог и без смены состояния запрещён. Обратимо повторной отправкой, но пользователь об этом не узнает. major NoopJobError— не ошибка. Значение «задач в этом состоянии нет» не логируется, не считается в метрику и не поднимает уровень. Нарушение даёт запись раз в секунду на каждый воркер. major- Миграция, уехавшая на сервер, не переписывается. Изменение — только новым файлом. Необратимо: goose считает применённую версию по номеру. critical
- Новая колонка правится во всех четырёх местах репозитория SQLite —
Create,Save,GetByID,FindAndAcquire. Компилятор расхождение не поймает, а проявится оно как потерянное при сохранении поле. major
Команды
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 строками
«Расхождение:».
Гейт
- Команда целиком:
task gate. База диффа — переменнаяBASE, по умолчаниюorigin/master; переопределяетсяtask gate BASE=<rev>. - Где логи шагов: вывод команды, отдельного файла нет.
- Что означает каждый исход: ненулевой код любого шага роняет гейт. У
docs.py check,tasks.py checkиopenspec.py checkсловарь кодов общий: 0 сошлось, 1 дрейф, 2 ошибка употребления, 3 окружение (не корень проекта, каталог не найден), 4 внутренний сбой. - Что красит безусловно и почему: отказ сборки, тестов,
go vet, неотформатированный файл, находкаgolangci-lint, дрейф раскладки документов, дрейф каталога задач, формаopenspec/config.yaml. Машина проверяет всё перечисленное, и это не обсуждается. Шаг, чей скрипт не найден, краснеет с именем недостающего плагина, а не пропускается молча. - Чего в гейте намеренно нет и кто тогда обязан это гонять:
gitleaks— висит на pre-commit вlefthook.ymlи смотрит только индекс коммита. Полную историю никто не проверяет;- согласованность документов между собой и с кодом — её судят агенты, зовёт
их скилл
av-dev-docs:healthcheck, и звать его надо руками; - покрытие изменённых строк не считается ничем.
Гейт на master сегодня зелёный целиком, и объявленных долгов у него нет.
Красный шаг означает поломку — свою или чужую, но поломку, а не наследство.
Списывать отказ на долг больше нельзя: списывать не на что.
Два прежних долга закрыты и здесь названы, чтобы отказ на их месте читался как новый:
golangci-lint runдавал 4 замечания — два непроверенныхCloseи два сравнения ошибок приведением типа. Закрыто задачейerrors-as-instead-of-typecast2026-08-11; тогда же уerrcheckвключена настройкаcheck-blank, поэтому_ = x.Close()больше не снимает замечание: отказ, который решено не проверять, объявляют вexclude-functionsпоимённо;go test ./...чинила задачаhttp-handler-tests-never-green.
Запреты
- Рабочую БД не трогать.
data/transcriber.dbна сервере и его копии. Локальная база в./data/— своя, её ронять и пересоздавать можно свободно. - Боевой каталог записей не трогать.
data/filesна сервере: там лежат голосовые сообщения живых людей. - Боевым токеном бота не запускаться. Второй процесс с тем же токеном перехватывает обновления у работающего, и пользователь теряет ответы.
- 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. - Необратимое (спрашивается у человека всегда): применённая миграция, формат
файла на диске и раскладка
data/files, публичный контракт HTTP API, имя ключа конфига, любое действие с боевыми данными и с Yandex Cloud, ротация секрета. - Что считается сломанным — новый красный шаг гейта, которого не было до твоей правки. Такое чинится прежде любой другой работы. Два объявленных долга из раздела «Гейт» сломанным состоянием не считаются, пока их не закрыли задачами.
- Ориентир по размеру порции: не замерялся.
- Что такое «сделана»:
task gateзелёный и критерии приёмки проверены поимённо.
Язык
- Документация, комментарии, сообщения коммитов — русский.
- Код и идентификаторы — английский.
- Текст, который видит пользователь Telegram, — русский.