# recognition Specification

## Purpose

Что принадлежит внешнему распознавателю и как это лежит у нас: попытка
распознавания отдельной строкой, сырой ответ провайдера целиком, структура
реплик, построенная из сохранённого ответа, и граница, за которую разбор чужого
формата не выходит.

Движение записи по рубежам нормирует `pipeline`, хранение текста и структуры —
`storage`, закрытость содержимого — `storage` же.

Сознательно не описан формат ответа конкретного провайдера: он живёт в адаптере
и в записках разведки, а не в норме поведения.
## Requirements

### Requirement: Попытка распознавания хранится отдельно от записи

Сервис SHALL держать всё, что принадлежит внешнему распознавателю, отдельной
строкой, связанной с аудиозаписью, и MUST не хранить это колонками самой записи.
К попытке относятся имя провайдера, имя модели, идентификатор операции у
провайдера, адрес, по которому провайдер читал аудио, время начала и время
завершения.

Разрез проходит по одной границе: **зависит ли вещь от провайдера
распознавания**. Идентификатор операции — самое провайдерское, что есть в
модели, а копия аудио во внешнем хранилище существует только потому, что
сегодняшний провайдер читает запись по адресу; другой провайдер её не потребует.
Оставленные колонками записи, они делают смену провайдера правкой доменной
сущности.

Копия аудио во внешнем хранилище MUST не считаться файлом записи: у записи
остаётся ровно две своих копии — принятая и приведённая, — а ключ объекта живёт
в строке попытки.

#### Scenario: Идентификатор операции лежит в попытке

- **GIVEN** запись отправлена на распознавание
- **WHEN** смотрят, где лежит идентификатор операции у провайдера
- **THEN** он лежит в строке попытки распознавания
- **AND** колонки с ним у самой записи нет

#### Scenario: Копия во внешнем хранилище не подменяет файл записи

- **GIVEN** запись прошла отправку на распознавание
- **WHEN** смотрят ссылки записи на файлы
- **THEN** они ведут на принятую и на приведённую копии
- **AND** ключ объекта во внешнем хранилище лежит в строке попытки

### Requirement: Сырой ответ провайдера сохраняется целиком

Сервис SHALL сохранять ответ распознавателя целиком, в том виде, в каком он
пришёл, и MUST хранить его **отдельным файлом в каталоге данных**, а не колонкой
строки попытки.

Хранится он потому, что **результат операции у провайдера не переспрашивается**:
связь реплики с говорящим сервис строить пока не умеет, и когда научится, архив
пересчитается из сохранённого без повторной оплаты.

Файлом, а не колонкой, — потому что шаг опроса читает строку попытки часто, а
репозиторий читает строку целиком: ответ на многочасовую запись, положенный
колонкой, ехал бы в память при каждом опросе. Где именно этот файл лежит,
нормирует capability `storage`, требование «Файл записи живёт в хранилище»:
третьим файлом в подкаталоге записи, наравне с копиями аудио. Копией аудио он при
этом не считается — их у записи по-прежнему две.

Чтение строки попытки шагом опроса MUST не тянуть за собой сохранённый ответ.

Сохранённый ответ — это полный текст речи, и закрыт он MUST быть наравне с самой
записью. Закрытость MUST держаться **проверкой владельца в обработчике сервиса**:
адреса, которым сохранённый ответ читают снаружи, сервис MUST не заводить вовсе, а
всякий адрес, отдающий содержимое записи, MUST судить владельца связанной
аудиозаписи сам, при каждом обращении. Пометка поля защищённым и правило
просмотра коллекции, которыми закрытость держалась прежде, — механизмы
встроенного хранилища, и их не остаётся; норма от этого не ослабла, а перестала
зависеть от настройки, которую мы не писали.

Путь к файлу сохранённого ответа MUST не попадать ни в журнал, ни в метку
метрики, ни в ответ отправителю.

Норму держит capability `storage`, требование «Содержимое записи закрыто везде,
где лежит»; здесь она названа потому, что попытка распознавания — то место, куда
содержимое приезжает впервые.

#### Scenario: Ответ сохранён и читается позже

- **GIVEN** распознавание завершилось и ответ провайдера получен
- **WHEN** запись доходит до конечного рубежа
- **THEN** сохранённый ответ доступен по строке попытки целиком

#### Scenario: Опрос не тянет сохранённый ответ

- **GIVEN** у попытки распознавания есть сохранённый ответ
- **WHEN** шаг опроса читает строку попытки
- **THEN** сохранённый ответ в память при этом не читается

#### Scenario: Адреса чтения сохранённого ответа у сервиса нет

- **GIVEN** запись принята одним узнанным и прошла распознавание
- **WHEN** другой узнанный ищет адрес, которым читается сохранённый ответ этой
  записи
- **THEN** такого адреса у сервиса нет
- **AND** содержимого он не получает

### Requirement: Структура реплик строится из сохранённого ответа

Сервис SHALL строить структуру реплик записи из сохранённого ответа провайдера и
MUST не обращаться к провайдеру повторно ради неё. Структура MUST хранить время
каждой реплики и MUST лежать отдельной строкой со ссылкой с записи, а не
колонкой записи.

У структуры MUST быть номер версии её вида: разбор сохранённого ответа изменится
раньше, чем архив пересчитают, и по номеру видно, какой разбор её построил.

Говорящих структура сегодня не размечает: связь реплики с разбором говорящего у
провайдера не выяснена. Требование этого и не заказывает — оно заказывает
источник, из которого разметка станет возможной без повторной оплаты.

#### Scenario: Структура собрана без обращения к провайдеру

- **GIVEN** ответ провайдера сохранён
- **WHEN** сервис строит структуру реплик
- **THEN** структура собрана с временем каждой реплики
- **AND** к провайдеру не уходит ни одного обращения

### Requirement: Разбор формата провайдера не выходит за адаптер

Распознаватель SHALL отдавать сервису доменный результат — реплики со временем,
плоский текст и байты ответа на хранение, — и MUST не отдавать сырой формат
провайдера. Ни один шаг конвейера MUST не знать, каким потоком и какими полями
провайдер отвечает.

Сегодня разбор потока лежит в шаге: адаптер отдаёт строку, склеенную из
альтернатив, и всё, что провайдер сказал сверх текста, теряется на границе
контракта.

#### Scenario: Шаг получает реплики, а не поток провайдера

- **WHEN** шаг конвейера забирает результат распознавания
- **THEN** он получает реплики со временем, плоский текст и байты на хранение
- **AND** формата провайдера в этом результате нет

### Requirement: Заливка и отправка на распознавание разделены

Сервис SHALL разделять укладку аудио туда, откуда провайдер его прочитает, и
отправку операции распознавания: это два разных обращения с разной ценой
повтора. Повтор укладки MUST быть бесплатен и класть объект под тем же ключом;
повтор отправки оплачивается наружу и MUST не происходить, когда операция уже
заведена.

Разделение нужно затем, чтобы шаг мог проверить сделанное прежде, чем платить:
объект нужного размера на месте — укладку MUST не повторять; идентификатор
операции в строке попытки есть — отправку MUST не повторять.

Строка попытки MUST заводиться **до** обращения к провайдеру: окно между ответом
провайдера и записью идентификатора — то место, где теряется оплаченное. Мягкую
остановку сервиса отправка MUST переживать своим пределом по времени; полной
защиты от жёсткого обрыва процесса требование не даёт и дать не может — это
остаточный риск, названный в дизайне, а не норма.

#### Scenario: Объект уже лежит, а операции ещё нет

- **GIVEN** аудио уже уложено туда, откуда провайдер его читает, и размер совпадает
- **AND** идентификатора операции в строке попытки нет
- **WHEN** шаг повторяется
- **THEN** укладка не повторяется
- **AND** операция отправляется

#### Scenario: Операция уже заведена

- **GIVEN** в строке попытки есть идентификатор операции
- **WHEN** шаг повторяется
- **THEN** отправка не повторяется
- **AND** шаг переходит к опросу этой операции

#### Scenario: Операция принята, а сервис мягко останавливают

- **GIVEN** отправка операции ушла провайдеру
- **AND** сервис в эту минуту останавливают мягко
- **WHEN** провайдер отвечает идентификатором операции
- **THEN** идентификатор сохраняется в строке попытки
- **AND** повторная отправка той же записи не заводится
