docs: устранены расхождения документов между собой и с кодом

- README разгружен: контракт HTTP API, таблицы БД, состояния задач и белый
  список отданы нормативным источникам ссылками
- в `architecture.md` и `review.md` числа и механика захвата задачи заменены
  ссылками на `database.md`, а описание конвейера ревью — на прогон от 2026-08-11
- в `CLAUDE.md` и `security.md` поправлены границы домена, оценка объёма записи
  и адрес очереди задач
This commit is contained in:
av
2026-08-11 12:24:41 +03:00
parent 2161d7f38e
commit ba7b4f37a6
6 changed files with 58 additions and 121 deletions
+9 -5
View File
@@ -14,9 +14,12 @@ HTTP API, — конвертирует её `ffmpeg` в ogg, отдаёт на
Yandex SpeechKit и возвращает текст туда, откуда пришла запись. Состояние задач
и метаданные файлов лежат в SQLite, файлы — на диске.
Чего **не** делает: не редактирует и не пересказывает текст, не хранит записи как
архив, не распознаёт речь сам, не заводит учётные записи и не работает с живым
потоком. Границу домена целиком держит [docs/passport.md](docs/passport.md).
Чего **не** делает: сам речь не распознаёт и своих моделей не держит, текст
руками не правит и в форматы документов не экспортирует, учётных записей не
заводит, с живым потоком не работает и складом произвольных файлов не служит.
Записи и расшифровки хранит бессрочно: решением от 2026-08-11 сервис — архив.
Перечень выше — выжимка, границу домена целиком держит
[docs/passport.md](docs/passport.md).
## Стек
@@ -106,8 +109,9 @@ task gate # весь набор проверок разом
Новые отказы отличай от этого. Пока он жив, «зелёный гейт» в определении
сделанного означает «не добавилось ничего сверх перечисленного».
**`go test ./...` больше долгом не считается.** Тесты приёма по HTTP чинены
задачей `http-handler-tests-never-green`; красный `go test` теперь означает
**`go test ./...` больше долгом не считается.** Задача
`http-handler-tests-never-green` починила тесты приёма по HTTP; красный
`go test` теперь означает
поломку, и списывать его на наследство нельзя.
## Запреты
+23 -100
View File
@@ -43,16 +43,10 @@
### Белый список Telegram
Бот отвечает только тем, кто перечислен в `[server] users_while_list`. В
`config.dist.toml` этого ключа нет — добавьте его сами:
```toml
[server]
users_while_list = ["@username"]
```
Значения сверяются со строкой автора сообщения из Telegram (`@username` либо имя
с фамилией), а не с числовым id.
Бот отвечает только тем, кто перечислен в конфиге. Кого и по какому признаку он
пускает — [docs/security.md](docs/security.md), «Что разграничивает доступ»;
известные прорехи образца конфига, включая недостающий ключ белого списка, —
[docs/conventions/config.md](docs/conventions/config.md).
## Деплой
@@ -66,76 +60,22 @@ inv pl -- transcriber
локально и едет на сервер через `docker save`/`load`, реестр не участвует.
Локально образ можно собрать и руками — `task image` даст `transcriber:dev`.
## API Endpoints
## HTTP API
### POST /api/audio
Четыре маршрута: `POST /api/audio` — приём записи, `GET /api/status/:id` —
готовность задачи, `GET /metrics` — метрики Prometheus с префиксом
`transcriber_`, `GET /health` — проверка живости.
Загружает аудиофайл и создает задачу на расшифровку. Отвечает `201 Created`.
**Параметры:**
- `audio` (form-data) - аудиофайл для расшифровки
**Пример запроса:**
```bash
curl -X POST \
http://localhost:8080/api/audio \
-F "audio=@/path/to/your/audio.mp3"
```
**Ответ:**
```json
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "created"
}
```
### GET /api/status/:id
Получает статус задачи расшифровки по ID. Поле `transcription_text` появляется,
когда задача перешла в `done`.
**Пример запроса:**
```bash
curl http://localhost:8080/api/status/550e8400-e29b-41d4-a716-446655440000
```
**Ответ:**
```json
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "done",
"created_at": "2024-01-01T12:00:00Z",
"transcription_text": "расшифрованный текст"
}
```
### GET /metrics
Метрики Prometheus с префиксом `transcriber_`.
### GET /health
Проверка работоспособности сервиса.
**Ответ:**
```json
{
"status": "ok",
"message": "Transcriber service is running"
}
```
Контракт приёма и опроса нормативен и живёт в
[openspec/specs/intake/spec.md](openspec/specs/intake/spec.md): поля запроса и
ответа, коды и условия. Менять его — необратимое действие
([CLAUDE.md](CLAUDE.md), «Работа»), и второго описания у него быть не должно.
## Состояния задач
- `created` - задача создана, файл сохранён, ждёт конвертации
- `converted` - файл сконвертирован в ogg, ждёт отправки на распознавание
- `transcribe` - распознавание запущено в Yandex SpeechKit, ждём результата
- `done` - задача завершена успешно
- `failed` - задача завершена с ошибкой
Задачи двигают три фоновых воркера: `conversion_worker`, `transcribe_worker` и
`check_worker`. Каждый опрашивает базу раз в секунду и делает один шаг.
Перечень состояний, переходы между ними и число воркеров —
[docs/database.md](docs/database.md), разделы «Таблицы» и «Представление
данных»; как сложен конвейер целиком — [docs/architecture.md](docs/architecture.md).
## Структура проекта
@@ -166,25 +106,9 @@ transcriber/
## База данных
### Таблица `files`
- `id` (TEXT) - UUID файла
- `storage` (TEXT) - где лежит файл: `local` или `s3`
- `file_name` (TEXT) - имя файла в хранилище
- `size` (INTEGER) - размер файла в байтах
- `created_at` (DATETIME) - время создания
### Таблица `transcribe_jobs`
- `id` (TEXT) - UUID задачи
- `state` (TEXT) - состояние задачи
- `source` (TEXT) - откуда пришла задача: `api`, `telegram` или `unknown`
- `file_id` (TEXT) - ссылка на текущий файл задачи
- `delay_time` (DATETIME) - не брать задачу раньше этого времени
- `acquisition_id`, `acquire_time` - захват задачи воркером
- `recognition_op_id` (TEXT) - ID операции распознавания в Yandex Cloud
- `transcription_text` (TEXT) - результат распознавания
- `is_error` (BOOLEAN), `error_text` (TEXT) - признак и текст ошибки
- `tg_chat_id`, `tg_reply_message_id` - куда отправить результат в Telegram
- `created_at`, `updated_at` (DATETIME) - времена создания и обновления
Две таблицы, `files` и `transcribe_jobs`. Колонки, ключи, правило времени и
идентификаторов, а также механика захвата задачи воркером —
[docs/database.md](docs/database.md).
## Разработка
@@ -204,12 +128,11 @@ goose -dir migrations sqlite3 data/transcriber.db up
goose -dir migrations sqlite3 data/transcriber.db down
```
Проверки перед коммитом:
Проверки перед коммитом — одной командой:
```bash
go build ./...
go vet ./...
gofmt -l .
go test ./...
golangci-lint run
task gate
```
Что она гоняет, чем краснеет и какой отказ считается объявленным долгом —
[CLAUDE.md](CLAUDE.md), раздел «Гейт».
+10 -7
View File
@@ -21,6 +21,7 @@
- **Очередь таблицей.** Состояние задачи лежит в SQLite, воркер забирает работу
запросом с захватом. Внешний брокер не заводим: нагрузка — единицы записей в
день (оценка владельца, не замер: `research/` пуст).
<!-- канон: поведение → openspec/specs/pipeline -->
- **Шаг конвейера идемпотентен по повтору.** Задача, брошенная на середине,
достаётся снова по истечении срока захвата и проходит шаг заново.
- **Ядро зависит от интерфейсов.** `internal/service` знает только
@@ -87,7 +88,8 @@
`transcriber_worker_job_count` с меткой `error="true"` и по логам контейнера.
Отдельного оповещения нет.
- **Характер потока:** непрерывный, но разреженный. Бот держит длинный опрос,
три воркера опрашивают базу раз в секунду вхолостую.
три воркера опрашивают базу вхолостую с паузой из
[database.md](database.md), «Настройки с числовым значением».
## Единые точки проекта
@@ -136,8 +138,9 @@
- **Долгие записи.** Потолок сегодня неизвестен и не замерялся: 20 МиБ на приём
из Telegram — точно, ограничения `deferred-general` по длине — нет. Расчётный
потолок проекта — шесть часов, и он взят с запасом, а не замером.
- **Приём большого файла.** Форма читается целиком, предел
`router.MaxMultipartMemory` — 32 МиБ, обрыв начинает загрузку заново.
- **Приём большого файла.** Форма читается целиком, предел памяти под multipart
задан числом в [database.md](database.md), «Настройки с числовым значением»;
обрыв начинает загрузку заново.
Загрузку частями разбирает разведка `chunked-upload-choice`; её выбор меняет
публичный контракт приёма и потому идёт через решение в `adr/`.
- **Учёт расхода.** Распознавание и языковая модель оплачиваются по факту, а
@@ -152,10 +155,10 @@
- **Видео.** Дорожку из видеофайла бот принимает по MIME-типу `video/`, но
конвертер этот случай не проверялся.
- **Очередь.** Принцип «очередь таблицей» и захват `FindAndAcquire` написаны
вручную: захват не транзакционен, повторов с нарастающей паузой нет, число
попыток не считается, очереди мёртвых задач нет. Пересматривается разведкой
`job-queue-choice` раньше смены хранилища, чтобы не переписывать захват
дважды.
вручную; чем именно захват несовершенен — [database.md](database.md),
«Представление данных». Не решено здесь другое: пересматривать ли модель
очереди целиком — разведка `job-queue-choice`, раньше смены хранилища, чтобы
не переписывать захват дважды.
- **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать
счётчики через `client_golang` или перейти на OpenTelemetry с трассировкой —
решает разведка `opentelemetry-fit`. Коллектор был бы процессом, которого в
+1 -1
View File
@@ -95,7 +95,7 @@ OpenSpec.
| Когда добавляем | Поля |
| --- | --- |
| на входящий HTTP-запрос | `transport` (`http`, `telegram`), `http.method`, `http.route`, `http.status_code`, `duration_ms` |
| на задачу | `capability` (значения — по именам заведённых capability в `openspec/specs/`; пока их нет, поле не заполняется), `job_id`, `file_id`, `source` |
| на задачу | `capability` (значения — по именам заведённых capability в `openspec/specs/`), `job_id`, `file_id`, `source` |
| на запись об ошибке | `error` |
| на вызов внешнего сервиса | `ext.service`, `ext.operation`, `ext.status_code`, `duration_ms`, `retry` |
+10 -5
View File
@@ -2,8 +2,11 @@
## Как настроен конвейер
Конвейера ревью в проекте пока нет: плагин не подключён, ни одного прогона не
было. Раздел заполнен наперёд по коду — он и служит настройкой первому прогону.
Конвейер ревью прогонялся один раз — 2026-08-11, на изменении
`fix-http-handler-tests`; его триаж лежит в
`openspec/changes/archive/2026-08-11-fix-http-handler-tests/review/triage.md`.
Разделы ниже заполнены наперёд по коду и правятся по итогам прогонов: «Типовые
ложноположительные» первым прогоном уже пользовались.
### Типовые узлы
@@ -71,8 +74,9 @@
[conventions/errors.md](conventions/errors.md).
- **«Захват задачи не в транзакции — гонка двух воркеров».** По построению её
нет: три воркера читают три разных состояния, и одну строку они не делят.
Находка становится настоящей ровно тогда, когда появится второй экземпляр
процесса или второй воркер на то же состояние.
Механика захвата и её слабые места — [database.md](database.md),
«Представление данных». Находка становится настоящей ровно тогда, когда
появится второй экземпляр процесса или второй воркер на то же состояние.
- **«Файлы и объекты не удаляются, диск растёт».** Факт верный и записан в
[database.md](database.md); срок хранения не задан сознательно, задачи на него нет.
Новой находкой это не считается, пока не измерен рост.
@@ -103,7 +107,8 @@
`createTranscribeJob` — сегодня через него идут оба входа
([architecture.md](architecture.md), «Единые точки проекта»).
- `architecture`: не поехало ли поведение в `architecture.md` вместо спеки —
спек ещё нет, и соблазн описать поведение в обзоре максимальный.
заведена одна capability (`openspec/specs/intake`), поведение прочих узлов
живёт в обзоре под маркерами долга, и соблазн дописать туда ещё максимальный.
- `conventions`: новая колонка правится во всех четырёх местах репозитория
(CLAUDE.md, «Инварианты»).
- `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня
+5 -3
View File
@@ -159,7 +159,8 @@ Telegram отправителю.
`internal/service/transcribe.go:107` кладёт `file_name` на общем шаге заведения
задачи, то есть для обоих входов. Это нарушение инварианта приватности из
`CLAUDE.md`, оно объявлено критическим, и чинит его задача
`no-user-filename-in-log` — первая строка очереди.
`no-user-filename-in-log`; её место в очереди
[tasks/BACKLOG.md](../tasks/BACKLOG.md).
Токен бота попадает в URL скачивания файла (`file.Link(token)`), и этот URL
нигде не логируется.
@@ -177,8 +178,9 @@ Telegram отправителю.
- **Стойкость к целенаправленной нагрузке.** Ограничения по числу запросов и по
размеру файла нет, и защищаться от исчерпания диска мы сейчас не пытаемся.
- **Исчерпание диска приглашёнными.** Записи и тексты хранятся бессрочно
(паспорт, 2026-08-11), шестичасовая запись весит гигабайты, а квот нет и не
будет: решено считать расход и показывать его владельцу, а не отказывать
(паспорт, 2026-08-11), шестичасовая запись весит единицы гигабайт — оценка, а не
замер: `research/` пуст, потолок длины стоит открытым вопросом
`architecture.md`, «Долгие записи», — а квот нет и не будет: решено считать расход и показывать его владельцу, а не отказывать
(цель `usage-stats`). Перебравшего останавливает разговор или отзыв доступа в
Authelia. Рост каталога `data/files` при этом ничем не наблюдается —
открытый вопрос `architecture.md`.