- адреса приложения переехали в своё пространство `/app/`, опрос готовности убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи, текст — отдельным адресом названного вида - заведена единая точка отображения доменной ошибки и слой, приводящий к той же форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением - у записи появились имя файла отправителя, длительность и размер своими колонками, а у ленты владельца — свой индекс: без него страница сканировала весь архив сервиса
190 lines
18 KiB
Markdown
190 lines
18 KiB
Markdown
## 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` с одним телом.
|
||
- Карточка остановленной записи несёт рубеж, признак остановки и её причину, а
|
||
машинного текста отказа не несёт. Оракул — тест на остановленной записи.
|