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

18 KiB
Raw Blame History

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 7.1 docs/database.md — три новые колонки записи
  • 7.2 docs/conventions/web-ui.md — правило неизвестного пути перечисляет корни сервиса (проверить, что запись уже верна)
  • 7.3 CLAUDE.md, инвариант «Принятая запись не теряется молча» — причина видна владельцу карточкой записи, а не опросом готовности: опрос убран, и инвариант ссылается на адрес, которого нет
  • 7.4 docs/passport.md — сценарии потребителей называют POST /api/audio и GET /api/status/:id действующими; задача api-tokens прочитает их как действующие и заведёт токен на несуществующий адрес
  • 7.5 docs/security.md — периметр и таблица поверхностей перечисляют убираемые адреса и не знают ни одного адреса приложения, а изменение добавляет их несколько
  • 7.6 docs/architecture.md — строка компонента «HTTP API» и строка «кто заметит отказ» называют опрос готовности; там же строка про done без текста
  • 7.7 Обновить мутационную проверку journal_route_test.go: пути /api/audio и /api/status/... в ней заменены новыми адресами приложения
  • 7.8 task gate зелёный целиком
  • 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 с одним телом.

  • Карточка остановленной записи несёт рубеж, признак остановки и её причину, а машинного текста отказа не несёт. Оракул — тест на остановленной записи.