From ba7b4f37a69d0d2620895db1b62f48c9f183f301 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Tue, 11 Aug 2026 12:24:41 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=83=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B6=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2=20=D0=BC=D0=B5=D0=B6=D0=B4=D1=83=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D0=BE=D0=B9=20=D0=B8=20=D1=81=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README разгружен: контракт HTTP API, таблицы БД, состояния задач и белый список отданы нормативным источникам ссылками - в `architecture.md` и `review.md` числа и механика захвата задачи заменены ссылками на `database.md`, а описание конвейера ревью — на прогон от 2026-08-11 - в `CLAUDE.md` и `security.md` поправлены границы домена, оценка объёма записи и адрес очереди задач --- CLAUDE.md | 14 ++-- README.md | 123 +++++++----------------------------- docs/architecture.md | 17 +++-- docs/conventions/logging.md | 2 +- docs/review.md | 15 +++-- docs/security.md | 8 ++- 6 files changed, 58 insertions(+), 121 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6f615f0..69db07a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` теперь означает поломку, и списывать его на наследство нельзя. ## Запреты diff --git a/README.md b/README.md index ef1f35d..9e218a3 100644 --- a/README.md +++ b/README.md @@ -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), раздел «Гейт». diff --git a/docs/architecture.md b/docs/architecture.md index 11ecaec..786c0aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,6 +21,7 @@ - **Очередь таблицей.** Состояние задачи лежит в SQLite, воркер забирает работу запросом с захватом. Внешний брокер не заводим: нагрузка — единицы записей в день (оценка владельца, не замер: `research/` пуст). + - **Шаг конвейера идемпотентен по повтору.** Задача, брошенная на середине, достаётся снова по истечении срока захвата и проходит шаг заново. - **Ядро зависит от интерфейсов.** `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`. Коллектор был бы процессом, которого в diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md index f76bdeb..dba7803 100644 --- a/docs/conventions/logging.md +++ b/docs/conventions/logging.md @@ -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` | diff --git a/docs/review.md b/docs/review.md index ec499b7..1c02154 100644 --- a/docs/review.md +++ b/docs/review.md @@ -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`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня diff --git a/docs/security.md b/docs/security.md index 85f10c0..55bb177 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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`.