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

190 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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` с одним телом.
- Карточка остановленной записи несёт рубеж, признак остановки и её причину, а
машинного текста отказа не несёт. Оракул — тест на остановленной записи.