- `docs/.pm.json` переименован в `docs/.docs.json`, версия канона — 14 - ADR принимает записку разведки как источник решения наравне с архивным `design.md` - заведён `tasks/.tasks.json`, а в описании гейта — шаги `tasks.py check` и `openspec.py check`
152 lines
11 KiB
Markdown
152 lines
11 KiB
Markdown
# 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 и возвращает текст туда, откуда пришла запись. Состояние задач
|
||
и метаданные файлов лежат в SQLite, файлы — на диске.
|
||
|
||
Чего **не** делает: не редактирует и не пересказывает текст, не хранит записи как
|
||
архив, не распознаёт речь сам, не заводит учётные записи и не работает с живым
|
||
потоком. Границу домена целиком держит [docs/passport.md](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**
|
||
- **Бот отвечает только тем, кто в белом списке.** Бот проверяет отправителя до
|
||
любой работы, включая скачивание файла. Нарушение обратимо правкой конфига, но
|
||
чужие записи к тому моменту уже обработаны за наши деньги. **critical**
|
||
- **Принятая запись не теряется молча.** Отказ на любом шаге либо оставляет
|
||
задачу пригодной к повтору, либо переводит её в `failed` и сообщает
|
||
пользователю. Молчаливый выход из шага без записи в лог и без смены состояния
|
||
запрещён. Обратимо повторной отправкой, но пользователь об этом не узнает.
|
||
**major**
|
||
- **`NoopJobError` — не ошибка.** Значение «задач в этом состоянии нет» не
|
||
логируется, не считается в метрику и не поднимает уровень. Нарушение даёт
|
||
запись раз в секунду на каждый воркер. **major**
|
||
- **Миграция, уехавшая на сервер, не переписывается.** Изменение — только новым
|
||
файлом. Необратимо: goose считает применённую версию по номеру. **critical**
|
||
- **Новая колонка правится во всех четырёх местах** репозитория SQLite —
|
||
`Create`, `Save`, `GetByID`, `FindAndAcquire`. Компилятор расхождение не
|
||
поймает, а проявится оно как потерянное при сохранении поле. **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` словарь кодов общий:
|
||
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` (`adapter/recognizer/yandex/speechkit.go:55`, `main.go:124`) и два
|
||
сравнения ошибок приведением типа (`controller/worker/worker.go:51`,
|
||
`service/transcribe.go:394`). Долг записан в
|
||
[docs/conventions/errors.md](docs/conventions/errors.md), заведён задачей
|
||
`errors-as-instead-of-typecast`.
|
||
|
||
Новые отказы отличай от этого. Пока он жив, «зелёный гейт» в определении
|
||
сделанного означает «не добавилось ничего сверх перечисленного».
|
||
|
||
**`go test ./...` больше долгом не считается.** Тесты приёма по HTTP чинены
|
||
задачей `http-handler-tests-never-green`; красный `go test` теперь означает
|
||
поломку, и списывать его на наследство нельзя.
|
||
|
||
## Запреты
|
||
|
||
- **Рабочую БД не трогать.** `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, — русский.
|