внутренняя модель перестроена вокруг аудиозаписи

- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
av
2026-08-14 20:20:33 +03:00
parent d079f03350
commit 1576d06735
84 changed files with 8973 additions and 2865 deletions
@@ -0,0 +1,102 @@
## Why
Сервис объявлен архивом: записи и расшифровки лежат бессрочно, к ним
возвращаются через месяцы, а поверх них строятся список, темы, уровни текста и
учёт расхода. Держать всё это негде — центральной сущности «аудиозапись» в
сервисе нет вовсе: есть задача конвейера, у которой поля захвата лежат в одной
строке с расшифровкой, указатель на файл переставляет каждый шаг, а отказ стирает
достигнутый рубеж и делает продолжение с места остановки невозможным.
Всякая задача, взятая раньше этой, будет переписана вместе с моделью — потому
владелец 2026-08-14 поставил её первой в план стройки.
## What Changes
- **Центральная сущность — аудиозапись.** Домен записи (владелец, заголовок,
краткое описание, рубеж, ссылки на приложения) отделяется от того, что нужно
только конвейеру, и от того, что принадлежит провайдеру распознавания.
- **Приложения к записи живут отдельными строками.** Файлы, тексты, структура
реплик, темы, журнал событий и попытка распознавания перестают быть колонками
одной строки и адресуются ссылками с записи.
- **Ссылки на файлы перестают переставляться.** У записи две отдельные ссылки —
на исходник и на приведённую копию, — и обе живут до конца. Сегодня их одна, и
прошедшая конвейер запись ведёт на объект во внешнем хранилище: послушать
загруженное нечем.
- **Копия во внешнем хранилище перестаёт быть файлом записи.** Она существует
только потому, что распознаватель читает аудио по адресу, и переезжает в
строку о попытке распознавания вместе с идентификатором операции.
- **Сырой ответ распознавателя сохраняется целиком** — вложением, а не колонкой.
Результат операции у провайдера не переспрашивается, а связь реплики с
говорящим сервис строить пока не умеет: когда научится, архив пересчитается из
сохранённого без единого рубля.
- **Состояние называет достигнутое, а не предстоящее.** Цепочка рубежей:
`uploaded → normalized → submitted → transcribed → done`. **BREAKING**: перечень
состояний в ответе о записи меняется целиком — публичный контракт HTTP API
объявлен проектом необратимым.
- **Остановка становится признаком, а не состоянием.** Прежние `failed` и `dead`
схлопываются в признак остановки с причиной; достигнутый рубеж при этом
сохраняется, и снятие признака продолжает работу с места остановки, а не с
начала. Снимает признак владелец панели — поверхности для владельца записи у
сервиса пока нет, её заводит задача с экранами.
- **Сторожей становится двое.** Число отказов ограничивает повторы внутри шага,
время в рубеже — застревание. Сегодня обе роли навешаны на счётчик попыток, и
он не справляется ни с одной: операция, зависшая у провайдера, опрашивается
вечно.
- **Предел времени в рубеже** — два числа: час на свою работу, сутки на чужую.
Достигнут предел — запись останавливается с причиной «застряла».
- **Переход и откладывание разводятся.** Шаг опроса перестаёт изображать переход
в то же самое состояние: откладывание ставит паузу и снимает захват, а рубежа и
времени входа в него не трогает.
- **Шаг с внешней оплатой проверяет сделанное** прежде, чем платить второй раз.
- **Воркеры теряют специализацию**, а их число задаётся настройкой; ноль —
законное значение: записи принимаются и не двигаются.
- **Переноса данных нет.** Сервис на сервере остановлен, прежние записи удалены
решением владельца 2026-08-14, и новая модель заводится с чистого листа.
## Capabilities
### New Capabilities
- `recognition`: попытка распознавания у внешнего провайдера — что о ней
хранится, почему сырой ответ сохраняется целиком, как из сохранённого строится
структура реплик и почему разбор формата провайдера не доходит до конвейера.
### Modified Capabilities
- `pipeline`: цепочка рубежей и смысл состояния; остановка признаком вместо
состояний отказа и смерти; два сторожа вместо одного; предел времени в рубеже;
разведение перехода и откладывания; необязательный шаг, чей отказ не роняет
запись; захват, возвращающий один идентификатор; воркер без специализации и его
число настройкой.
- `storage`: аудиозапись центральной сущностью и её приложения отдельными
коллекциями; две отдельные ссылки на файлы вместо одной переставляемой;
словарь тем на каждого владельца; журнал событий записи; перенос живых записей
шагом схемы.
- `intake`: перечень состояний в ответе о приёме и об опросе готовности.
## Impact
- **Схема хранилища**: новый шаг — коллекции `audio_records`, `texts`,
`structures`, `recognitions`, `record_events`, `topics`; прежняя
`transcribe_jobs` уходит; правка `files`. Применённые шаги не переписываются.
- **Публичный контракт HTTP API**: значения поля состояния. Необратимо.
- `internal/entity` — сущность записи, перечень рубежей, переходы, откладывание,
остановка признаком.
- `internal/contract` — распознаватель отдаёт доменный результат вместо строки,
заливка и отправка разделены; контракты репозиториев записи, файлов, текстов,
структуры, попыток распознавания и журнала.
- `internal/adapter/recognizer/yandex` — разбор потока результата в реплики,
раздельные заливка и отправка, отдача сырых байтов на хранение.
- `internal/adapter/repo/pocketbase` — запрос захвата, отображение записи,
правила панели.
- `internal/service` — шаги, таблица выбора следующего шага по рубежу, остановка
признаком.
- `internal/controller/worker` и `main.go` — пул вместо трёх именованных
воркеров.
- `internal/config` и `config.example.toml` — число воркеров, срок захвата по
шагу, два предела времени в рубеже.
- `docs/architecture.md`, `docs/database.md`, инварианты `CLAUDE.md` о колонках
очереди и о держателе захвата.
- **Предшествующая задача**: `external-call-timeouts` в плане стройки стоит
ниже, а по «Рамкам» постановки предшествует — без предела по времени у шага
срок захвата не может его превысить.