приём и чтение записей сведены к одному контракту приложения

- адреса приложения переехали в своё пространство `/app/`, опрос готовности
  убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи,
  текст — отдельным адресом названного вида
- заведена единая точка отображения доменной ошибки и слой, приводящий к той же
  форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением
- у записи появились имя файла отправителя, длительность и размер своими
  колонками, а у ленты владельца — свой индекс: без него страница сканировала
  весь архив сервиса
This commit is contained in:
av
2026-08-15 13:51:23 +03:00
parent 79ff12548f
commit 3a2da3004b
55 changed files with 5506 additions and 466 deletions
@@ -0,0 +1,104 @@
## MODIFIED Requirements
### Requirement: Аудиозапись — центральная сущность хранилища
Хранилище SHALL держать аудиозапись отдельной сущностью, а всё, что к ней
приложено, — отдельными строками со ссылками с записи. Приложениями считаются
файлы, тексты, структура реплик, темы, журнал событий и попытка распознавания.
Поля, которыми распоряжается очередь — признак захвата, срок его протухания,
пауза, число отказов, время входа в рубеж, — MUST не соседствовать с содержимым
записи в одной строке настолько, чтобы чтение очереди тянуло содержимое: сегодня
расшифровка лежит колонкой той же строки и читается при каждом захвате.
Запись MUST нести заголовок и краткое описание своими колонками: они читаются
вместе со списком, сотней штук разом. Расшифровка и вычитанный текст MUST лежать
отдельными строками: они читаются по открытию одной записи.
Тем же доводом запись MUST нести своими колонками **имя файла, данное
отправителем, длительность и размер**. Все три показываются в списке. Приём
узнаёт длительность и размер у источника метаданных и так, а имя файла приходит
вместе с записью.
**Имена колонок и единицы измерения нормативны:** `original_filename`,
`duration_ms` (миллисекунды) и `size_bytes` (байты). Единица стоит в самом имени,
а не в комментарии: шаг схемы применённым не переписывается, а расхождение
«секунды против миллисекунд» между колонкой, ответом списка и объявленным
пределом не увидит ни компилятор, ни гейт — оба конца числа. Миллисекунды выбраны
потому, что этой единицей уже названы соседние колонки схемы.
**Различать «неизвестно» и «ноль» эти колонки не обязаны, и это решение, а не
недосмотр.** Числовая колонка хранилища пустого значения не держит вовсе: пустое
кладётся нулём, и норма, требующая отличимости, потребовала бы либо четвёртой
колонки-признака, либо текстового типа у чисел. Платить за это нечем: обе
величины ставит приём, и ставит всегда — запись, метаданные которой прочитать не
удалось, отвергается отказом и не заводится вовсе. Ноль в этих колонках означает
ноль. Решение владельца 2026-08-15.
Имя файла на записи и заголовок MUST лежать **разными колонками**. Заголовок
несёт название, которое дал человек либо посчитала языковая модель; имя файла —
то, по чему человек узнаёт свою запись, пока заголовка нет. Одной колонкой на оба
смысла посчитанное название затирало бы имя, и вернуть затёртое было бы неоткуда.
Величины на записи и на её файле расходятся по смыслу, и **равенство между ними
не поддерживается никем — намеренно**. На записи лежит снимок **принятого**,
взятый приёмом один раз и больше не пересчитываемый; на файле — величины той
копии, которой файл является сейчас. Приведённая копия имеет свой размер, и
записи он не принадлежит.
Отсюда норма, без которой два числа читались бы как копии одного: величины
записи MUST не сверяться со строкой файла и MUST не переписываться ничем после
приёма. Расхождение между ними — не поломка, а разные вопросы: «что человек
прислал» и «что лежит сейчас». Уточнение длительности — перечитали метаданные,
сменили источник, нарезали длинную запись — меняет вторую величину и не трогает
первую.
#### Scenario: Список читается без содержимого
- **GIVEN** у записи есть расшифровка
- **WHEN** читают запись ради её рубежа и заголовка
- **THEN** текст расшифровки при этом не читается
#### Scenario: Длительность и размер читаются без строки файла
- **GIVEN** запись принята
- **WHEN** читают её длительность и размер
- **THEN** строка файла при этом не читается
#### Scenario: Посчитанный заголовок не затирает имя файла
- **GIVEN** запись принята с именем файла отправителя
- **WHEN** записи проставляют заголовок
- **THEN** имя файла остаётся прежним
### Requirement: Тексты и структура лежат отдельно от записи
Хранилище SHALL держать тексты записи отдельными строками, каждая со своим видом
текста, и структуру реплик — своей строкой. Запись MUST ссылаться на них, а не
хранить их колонками.
Видов текста больше одного: сырая расшифровка и вычитанный текст. Колонкой на
каждый вид схема росла бы с каждым новым видом, а необратимый шаг схемы платится
за каждую такую колонку отдельно.
**Приложение MUST быть уникально по паре «запись и вид»**, а структура — по паре
«запись и версия разбора». Шаг завершения пишет текст, структуру и сохранённый
ответ несколькими операциями и только потом двигает рубеж: прерванный на середине
и повторённый с прежнего рубежа, он завёл бы второй комплект строк, и вопрос
«какой текст отдавать человеку» стал бы вопросом порядка записи, а не состояния.
Потребитель текста MUST называть **вид**, который берёт, а не брать последний
записанный: иначе исход зависит от порядка записи. Адрес, которым текст уходит
приложению, называет вид запросом — норму держит capability `archive`.
#### Scenario: Расшифровка лежит своей строкой
- **GIVEN** запись прошла распознавание
- **WHEN** смотрят, где лежит текст расшифровки
- **THEN** он лежит отдельной строкой, на которую запись ссылается
#### Scenario: Повтор шага не заводит второй расшифровки
- **GIVEN** шаг завершения записал расшифровку и оборвался до смены рубежа
- **WHEN** шаг повторяется с прежнего рубежа
- **THEN** строка расшифровки у записи одна