## 1. Схема и модель записи

- [x] 1.1 Завести шаг схемы под три колонки записи: `original_filename`,
  `duration_ms`, `size_bytes`. Один шаг на все три; применённый шаг не
  переписывается. Единица стоит в имени колонки
- [x] 1.2 Добавить три поля в `entity.AudioRecord` с комментарием о разрезе
  «имя файла против заголовка»; неизвестную длительность отличить от нулевой
- [x] 1.3 Провести колонки через оба места правки в пакете хранилища:
  `applyToRecord` кладёт их (это заведение), `applyOwnedByPipeline` — не трогает
  (конвейер их не меняет), `recordToAudioRecord` читает
- [x] 1.4 Добавить темы записи в модель и в чтение записи; в запись их не кладёт
  ни приём, ни конвейер
- [x] 1.5 Прогнать `schema_test.go` и сверку правил `internal/archrules`

## 2. Приём: имя файла, длительность и размер

- [x] 2.1 Завести предел длины имени отправителя и уборку управляющих знаков
  одним местом в домене; предел назвать константой с обоснованием
- [x] 2.2 Записать в принимаемую запись очищенное имя, длительность и размер;
  колонку заголовка оставить пустой
- [x] 2.3 Проверить тестом, что имя отправителя не появилось ни в имени файла
  хранилища, ни в журнале — прежние тесты приватности остаются зелёными

## 3. Отображение доменной ошибки в ответ

- [x] 3.1 Завести доменный признак «присланная запись негодна» и обернуть им
  отказ источника метаданных в службе приёма
- [x] 3.2 Написать единую функцию отображения доменной ошибки в код ответа и
  человекочитаемое сообщение по таблице из `docs/conventions/errors.md`
- [x] 3.3 Завести единую форму тела отказа и провести через неё все ветви:
  ненайденная запись, негодный ввод, отсутствие учётной записи, сбой хранилища
- [x] 3.4 Завести доменный признак «запись сверх потолка размера», код `413`,
  предел в теле числом
- [x] 3.5 Убрать опечатку `transcibe` из текста ошибки приёма
- [x] 3.6 Завести закрытый перечень машиночитаемых кодов отказа и отдавать их
  вторым полем тела рядом с сообщением человеку
- [x] 3.7 Назвать точку отображения в `docs/architecture.md`, «Единые точки
  проекта», и снять расхождение в `docs/conventions/errors.md`
- [x] 3.8 Дописать в таблицу `docs/conventions/errors.md` ветви, которых там нет:
  `401`, `403`, `413` и машиночитаемый код отказа — объявленный источник единой
  точки иначе расходится с ней в первый же день

## 4. Пространство адресов приложения

- [x] 4.1 Перевесить группу маршрутов с чужого корня на `/app/`, слой сессии
  повесить на группу корня, а не на перечень адресов
- [x] 4.2 Завести своё правило ограничителя частоты под корень приложения
- [x] 4.3 Убрать прежние адреса `POST /api/audio` и `GET /api/status/{id}`
  целиком

## 5. Адреса чтения

- [x] 5.1 `GET /app/me` — идентификатор учётной записи и имя к показу, без
  адреса почты
- [x] 5.2 `GET /app/config` — потолок размера тем же числом, каким сервис
  ограничивает тело запроса, частота опроса, перечень известных расширений,
  потолок тем
- [x] 5.3 `GET /app/audiorecords` — страница своих записей новыми сверху, с
  ключом следующей страницы и общим числом; отбор тремя состояниями
- [x] 5.4 `GET /app/audiorecords/{id}` — карточка записи без текста, с перечнем
  доступных видов текста `available_views`; пустой перечень значит «текста ещё
  нет»
- [x] 5.5 `GET /app/audiorecords/{id}/text` — текст названного вида из закрытого
  перечня `transcript`, `literary`, `replicas`; неизвестный и незаданный вид
  дают `400`, отсутствующий у записи вид — `409`
- [x] 5.6 `POST /app/audiorecords` — ответ списком с местом под признак
  повторного файла, элемент той же формы, что и карточка

## 6. Отбор и чтение в хранилище

- [x] 6.1 Добавить в репозиторий записей выборку по владельцу курсором на паре
  «время заведения и идентификатор», новыми сверху, с умолчанием и потолком
  размера страницы
- [x] 6.2 Проверить тестом, что выборка списка не читает ни строки текста, ни
  структуры реплик
- [x] 6.3 Разрешить темы записи названиями одним запросом на страницу, а не по
  запросу на запись
- [x] 6.4 Вывести предикаты трёх состояний отбора («в работе», «остановлена»,
  «прошла конвейер») из дескриптора рубежа в `internal/entity`, а не строкой
  фильтра в репозитории
- [x] 6.5 Дописать правило `internal/archrules` на нового потребителя словаря
  рубежей — отбор списка

## 7. Документация и гейт

- [x] 7.1 `docs/database.md` — три новые колонки записи
- [x] 7.2 `docs/conventions/web-ui.md` — правило неизвестного пути перечисляет
  корни сервиса (проверить, что запись уже верна)
- [x] 7.3 `CLAUDE.md`, инвариант «Принятая запись не теряется молча» — причина
  видна владельцу **карточкой записи**, а не опросом готовности: опрос убран, и
  инвариант ссылается на адрес, которого нет
- [x] 7.4 `docs/passport.md` — сценарии потребителей называют `POST /api/audio` и
  `GET /api/status/:id` действующими; задача `api-tokens` прочитает их как
  действующие и заведёт токен на несуществующий адрес
- [x] 7.5 `docs/security.md` — периметр и таблица поверхностей перечисляют
  убираемые адреса и не знают ни одного адреса приложения, а изменение добавляет
  их несколько
- [x] 7.6 `docs/architecture.md` — строка компонента «HTTP API» и строка «кто
  заметит отказ» называют опрос готовности; там же строка про `done` без текста
- [x] 7.7 Обновить мутационную проверку `journal_route_test.go`: пути `/api/audio`
  и `/api/status/...` в ней заменены новыми адресами приложения
- [x] 7.8 `task gate` зелёный целиком
- [x] 7.9 Поднять сервис локально: шаг схемы применён на чистой базе, адреса
  приложения подняты, отказ без сессии идёт единой формой, прежние адреса
  отвечают `404`. Сценарий вошедшего локально не проходится по устройству
  проекта: сессию выдаёт только провайдер OIDC, а локальный запуск наружу не
  ходит — этот путь закрыт машиной в тестах, через настоящий роутер

## Критерии приёмки

Два источника, и приёмка судится по одному списку. Первый блок — из записи
задачи `json-api-for-spa` дословно: файл задачи закрытие удалит, критерии
обязаны его пережить. Второй — рубрика ревью дизайна, дополняющая их там, где
постановка молчала.

### От постановки

- Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при
  чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым
  текстом, а не `500`, а отказ приёма по пустому владельцу — `403`, как и отказ
  слоя предъявления. Тело ошибки при этом одной формы на всех адресах, без
  сырого `err.Error()` и без опечатки `transcibe`, а отображение живёт в одной
  функции, названной в `docs/architecture.md`. Оракул — три теста на код
  (репозиторий с ошибкой драйвера; файл, который отвергает разбор метаданных;
  вызов приёма без учётной записи), тест формы на четырёх ветвях отказа, пустой
  `grep -rn 'transcibe' internal/` и `task gate`, шаг `docs.py check`.
- Список отдаётся страницами, новые сверху, и несёт заголовок, имя файла
  отправителя, длительность, размер, рубеж с признаком остановки и темы, не
  читая при этом ни расшифровки, ни структуры реплик. Оракул — два теста:
  выборка больше страницы; запись с расшифровкой, у которой чтение списка не
  трогает строку текста.
- Ответ приёма отдаёт список заведённых записей и место под признак повторного
  файла, даже когда файл в запросе один, а имя файла отправителя лежит своей
  колонкой — обрезанное по пределу и без управляющих знаков — при пустом
  заголовке. Оракул — четыре теста приёма: тело ответа — список из одного
  элемента с полем признака повтора; имя `разговор.mp3` доходит до
  `original_filename`; колонка заголовка у принятой записи пуста; имя длиннее
  предела и с управляющими знаками доходит обрезанным и очищенным.
- `GET /app/config` отдаёт потолок размера тем же числом, каким сервер отвергает
  запись сверх него, а не своей копией. Оракул — тест: значение в ответе равно
  `entity.MaxRecordSize`, и подмена константы меняет ответ.
- Карточка записи и её текст читаются порознь, а прежний опрос готовности
  отвечает `404`: карточка несёт перечень доступных видов текста, а текст
  отдаётся названным видом. Оракул — четыре теста: карточка без поля текста, но с
  перечнем доступных видов; текст вида `transcript`; текст вида `replicas`;
  прежние адреса `GET /api/status/{id}` и `POST /api/audio` на заведённой записи
  отвечают `404`.

  *Правка от 2026-08-15:* критерий приведён к спеке после повторной сверки.
  Прежняя формулировка требовала признак наличия текста и «тот же вид репликами»
  — обе воскрешали то, что ревью дизайна уже закрыло: булев признак не выражает
  состояния «текст есть, реплик нет», а `transcript` репликами не отдаётся, у них
  своё значение перечня.

### От рубрики ревью дизайна

- Страница задаётся непрозрачным ключом на паре «время заведения и
  идентификатор», и запись, заведённая между двумя страницами, окна не сдвигает.
  Оракул — два теста: прочитать первую страницу, завести запись, прочитать
  следующую ключом — ни повторов, ни пропусков; две записи с одинаковым временем
  заведения приходят в одном и том же порядке при повторном запросе.
- Карточка и элемент страницы — одна форма, а элемент ответа приёма равен
  карточке плюс признак повтора. Оракул — два теста: набор полей карточки
  совпадает с набором полей элемента страницы, кроме `available_views`; набор
  полей элемента ответа приёма равен набору полей карточки плюс `duplicate`.
- Размер страницы: умолчание применяется, значение сверх потолка усекается,
  негодное отвергается. Оракул — три теста: запрос без размера; запрос размером
  сверх потолка; запрос размером ноль даёт `400`.
- Отбор различает три состояния, и остановленная запись приходит ровно в одном
  из них. Оракул — тест на трёх записях: в работе, остановленная, прошедшая
  конвейер; каждая приходит ровно один раз.
- «Текста этого вида ещё нет» отличимо от «записи нет». Оракул — тест: запись с
  расшифровкой и без структуры на вид `replicas` даёт `409`, а неизвестный
  идентификатор — `404`.
- Отказ по превышению потолка размера проходит через единую форму. Оракул —
  тест: тело сверх `entity.MaxRecordSize` даёт `413`, а тело ответа несёт код
  отказа, сообщение и предел числом.
- Колонки названы с единицей в имени. Оракул — шаг схемы заводит `duration_ms` и
  `size_bytes`; тест приёма проверяет, что обе величины доходят до записи.

  *Правка от 2026-08-15:* требование «неизвестная длительность отличима от
  нулевой» снято решением владельца после ревью кода. Числовая колонка хранилища
  пустого значения не держит — проверено прогоном, — а платить за отличимость
  четвёртой колонкой или текстовым типом не за что: обе величины ставит приём и
  ставит всегда.
- Отсутствие сессии не даёт различить заведённую запись и неизвестную. Оракул —
  тест: оба запроса без сессии дают `401` с одним телом.
- Карточка остановленной записи несёт рубеж, признак остановки и её причину, а
  машинного текста отказа не несёт. Оракул — тест на остановленной записи.
