Files
transcriber/openspec/changes/archive/2026-08-15-app-json-contract/specs/storage/spec.md
T
av 3a2da3004b приём и чтение записей сведены к одному контракту приложения
- адреса приложения переехали в своё пространство `/app/`, опрос готовности
  убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи,
  текст — отдельным адресом названного вида
- заведена единая точка отображения доменной ошибки и слой, приводящий к той же
  форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением
- у записи появились имя файла отправителя, длительность и размер своими
  колонками, а у ленты владельца — свой индекс: без него страница сканировала
  весь архив сервиса
2026-08-15 13:51:23 +03:00

8.8 KiB

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 строка расшифровки у записи одна