Кнопка «Тип» была только в Telegram (доменная SetType = жёсткий override media_type + хинт + перераспознавание), в вебе её нет. Это создавало расхождение поверхностей и внутреннюю противоречивость спеки review. Решение: смена типа — редкий случай, для него достаточно «Уточнить» (перераспознавание с явным указанием типа). Явный переключатель не нужен ни на одной поверхности. Снято: Telegram-кнопка и callback type:, worker.SetType, ставший мёртвым override-плумбинг media_type (ovrMediaType, ветка applyOverrides, хелпер oppositeType) и стейл-хвосты в тестах httpapi. Спека review — три MODIFIED требования (запрет на все поверхности, «фиксация типа» убрана из команд и из быстрых действий Telegram, иллюстрация override заменена на закрепление источника). Синхронизирован docs/specs/review-ux.md. Change заархивирован: openspec/changes/archive/2026-07-18-review-remove-type-switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
413 lines
34 KiB
Markdown
413 lines
34 KiB
Markdown
# review Specification
|
||
|
||
## Purpose
|
||
Ревью раскладки человеком после распознавания и матча: петля «догадка →
|
||
подсказка → перераспознавание», команды (Применить/Уточнить/Распознать заново/
|
||
Игнор/Позже/Отклонить/Undo/Привязать заново; тип — read-only, корректируется
|
||
через «Уточнить»), мягкие подсказки vs жёсткие
|
||
`override`, единый список источников совпадения с ручным добавлением и
|
||
предпросмотром (превью = применение), разделение труда транспортов (веб —
|
||
точные правки, Telegram — быстрые действия и эскалация в веб).
|
||
## Requirements
|
||
### Requirement: Вход в review с явной причиной
|
||
|
||
Когда модель уверенности не разрешает авто-раскладку, система SHALL переводить
|
||
загрузку в `review` и SHALL показывать **конкретную причину** (низкая самооценка
|
||
LLM; нет матча в базе или несколько кандидатов; предупреждение структурной
|
||
валидации; неразобранный ответ LLM), а не обобщённое «не уверен». Поверхность
|
||
решения SHALL быть единой для всех транспортов и содержать источник (имя, контекст,
|
||
дерево файлов), догадку системы (тип, название, год, матч) и превью целевой
|
||
раскладки.
|
||
|
||
#### Scenario: Причина видна в интерфейсе
|
||
|
||
- **GIVEN** загрузка ушла в `review` из-за отсутствия матча в базе
|
||
- **WHEN** пользователь открывает ревью
|
||
- **THEN** показана конкретная причина (напр. «нет в TMDB · уверенность 0.46»)
|
||
|
||
### Requirement: Команды ревью и их эффекты
|
||
|
||
Экран ревью SHALL предоставлять команды: **Применить** (создать хардлинки по
|
||
эффективному плану), **Уточнить** (добавить подсказку → перераспознать),
|
||
**Распознать заново** (повторный прогон без новой подсказки), **Игнор файла**,
|
||
**Позже** (`deferred`), **Отклонить** (`cancelled`), **Undo** (снять созданные
|
||
ссылки → `reverted`) и **Привязать заново** (из
|
||
`reverted`/`cancelled`/`target_missing` → перераспознавание с ручным
|
||
подтверждением). Никакая поверхность ревью — ни веб-UI, ни Telegram — MUST NOT
|
||
содержать команду переключения типа movie↔series: тип показывается read-only, а
|
||
его корректировка выполняется мягкой подсказкой через **Уточнить**
|
||
(перераспознавание, где пользователь явно указывает тип). Команды из любого
|
||
транспорта SHALL сериализоваться worker'ом под единой блокировкой; применяется
|
||
последняя валидная команда.
|
||
|
||
Команда **Позже** (`Defer`) SHALL парковать задачу в `deferred` из любого
|
||
не-терминального состояния, у которого уже есть раздача в qBittorrent, и SHALL
|
||
отклонять её из **пре-источникового** состояния `catched` (торрент ещё НЕ
|
||
добавлен в qBittorrent) — конфликтом (`ErrConflict`) с понятным пользователю
|
||
сообщением, НЕ меняя состояние загрузки. Пре-источниковое `catched` —
|
||
единственное состояние без раздачи среди не-терминальных: откладывать в нём
|
||
нечего (задача ещё не дошла до ревью), а `catched → deferred` уводил бы задачу в
|
||
лимбо — `processCatched` листает только `catched` и больше её не подхватит, а
|
||
последующие команды через отсутствие источника выводят необратимый `deleted`.
|
||
Терминальные состояния Defer SHALL отклонять как и прежде (`ErrConflict`).
|
||
|
||
Команды, которым нужен источник (**Применить**, **Уточнить**, **Распознать
|
||
заново**, **Привязать заново**), SHALL синхронно (без дебаунса) проверять перед
|
||
действием, что источник не только присутствует в qBittorrent, но и **готов к
|
||
раскладке** — раздача в готовом классе состояния
|
||
(`uploading`/`stalledUP`/`pausedUP`/… с учётом различий имён qBit v4/v5),
|
||
т.е. файлы докачаны. Если источник ещё качается (любое `downloading`-подобное
|
||
или переходное `moving`/`checking` состояние), команда SHALL отказывать с
|
||
конфликтом и причиной «торрент ещё качается», НЕ создавая хардлинки и НЕ меняя
|
||
состояние загрузки (её нахождение в `review`/`deferred`/… легитимно, приводить
|
||
к реальности нечего). Отсутствие источника в qBittorrent SHALL по-прежнему
|
||
приводить состояние к реальности (`orphaned`/`deleted`) и отказывать. Так
|
||
недокачанная задача не может пройти через перераспознавание в авто-раскладку
|
||
или ручное применение и захардлинкать неполные файлы, обойдя финальность
|
||
состояния `completed`.
|
||
|
||
#### Scenario: Применение создаёт раскладку
|
||
|
||
- **GIVEN** загрузка в `review` с эффективным планом
|
||
- **WHEN** пользователь выбирает «Применить»
|
||
- **THEN** создаются хардлинки по плану, задача переходит к раскладке
|
||
|
||
#### Scenario: Отклонить и привязать заново
|
||
|
||
- **GIVEN** загрузка в `review`
|
||
- **WHEN** пользователь «Отклонить», затем «Привязать заново»
|
||
- **THEN** задача уходит в `cancelled`, а затем снова на распознавание с ручным
|
||
подтверждением (авто-раскладка не делается)
|
||
|
||
#### Scenario: Тип не переключается командой ни в одном транспорте
|
||
|
||
- **GIVEN** загрузка в `review` с распознанным типом
|
||
- **WHEN** пользователь открывает ревью в вебе или в Telegram
|
||
- **THEN** отдельной команды/кнопки переключения movie↔series нет ни на одной
|
||
поверхности
|
||
- **AND** тип показан read-only; для смены типа пользователь уточняет
|
||
распознавание («Уточнить», явно указав тип)
|
||
|
||
#### Scenario: Позже паркует задачу из ревью
|
||
|
||
- **GIVEN** загрузка в `review` (раздача в qBittorrent уже есть)
|
||
- **WHEN** пользователь выбирает «Позже»
|
||
- **THEN** задача переходит в `deferred` и возвращается на поверхность ревью по
|
||
любому последующему действию
|
||
|
||
#### Scenario: Позже отклоняется для пре-источникового catched
|
||
|
||
- **GIVEN** загрузка в `catched` (торрент ещё не добавлен в qBittorrent)
|
||
- **WHEN** приходит команда «Позже» (`Defer`, напр. прямым POST на
|
||
`/ui/downloads/{id}/defer`)
|
||
- **THEN** команда отклоняется конфликтом с понятным сообщением, что отложить
|
||
можно только после добавления торрента
|
||
- **AND** загрузка остаётся в `catched` и штатно доходит до `downloading` через
|
||
`processCatched`
|
||
|
||
#### Scenario: Недокачанный источник отклоняет перераспознавание
|
||
|
||
- **GIVEN** загрузка припаркована в `deferred`, а её раздача в qBittorrent ещё
|
||
качается (`downloading`, файлы не докачаны)
|
||
- **WHEN** пользователь выбирает «Распознать заново» (или «Уточнить»/«Привязать
|
||
заново»)
|
||
- **THEN** команда отклоняется с конфликтом и причиной «торрент ещё качается»
|
||
- **AND** загрузка остаётся в `deferred`, хардлинки не создаются, авто-раскладка
|
||
не запускается
|
||
|
||
#### Scenario: Недокачанный источник отклоняет ручное применение
|
||
|
||
- **GIVEN** загрузка в `review`, чья раздача в qBittorrent ещё качается
|
||
- **WHEN** пользователь выбирает «Применить»
|
||
- **THEN** команда отклоняется с конфликтом «торрент ещё качается», хардлинки
|
||
на неполные файлы не создаются, состояние загрузки не меняется
|
||
|
||
### Requirement: Подсказка мягкая, override жёсткий
|
||
|
||
Подсказка (`hint`) SHALL быть мягким сигналом — её интерпретирует LLM при
|
||
перераспознавании. Ручная правка поля SHALL быть жёстким **override**: система
|
||
берёт значение как есть и «пиннит» его; перераспознавание НЕ SHALL затирать уже
|
||
поправленное поле. Накопленные подсказки и правки SHALL переживать
|
||
перераспознавание и накладываться на новый план.
|
||
|
||
#### Scenario: Override переживает перераспознавание
|
||
|
||
- **GIVEN** пользователь закрепил источник (кандидат метабазы) как эффективный
|
||
матч
|
||
- **WHEN** запускается перераспознавание по новой подсказке
|
||
- **THEN** в новом эффективном плане закреплённые название/год/провайдер
|
||
остаются
|
||
|
||
### Requirement: Единый список источников совпадения на ревью
|
||
|
||
Экран ревью (`/review/{id}`) SHALL показывать совпавшие источники **единым
|
||
списком**, в котором распознавание нейронкой (без базы) — такая же строка,
|
||
как кандидаты метабаз (TMDB/TVDB/TVMaze), а не отдельный режим сверху.
|
||
Ровно один источник в списке SHALL быть отмечен активным (эффективный
|
||
матч). Активный источник SHALL выбираться **одним кликом/тапом по строке
|
||
варианта** (радиокнопка), без отдельной кнопки подтверждения выбора. Выбор
|
||
источника SHALL сохранять его как эффективный матч (persist) и SHALL
|
||
выполняться через раундтрип на сервер (форма/htmx), без клиентского пересчёта
|
||
доменного состояния; при этом инфо-часть и предпросмотр раскладки SHALL
|
||
немедленно обновляться под выбранный источник (частичный своп блока, без полной
|
||
перезагрузки страницы). Тем же ответом свопа SHALL синхронно обновляться панель
|
||
действий — в частности доступность команды **Применить**, зависящая от наличия
|
||
предпросмотра раскладки, — через out-of-band-фрагмент, чтобы кнопка не
|
||
рассинхронизировалась с блоком источника (например при пустом предпросмотре
|
||
из-за коллизии путей). Экран SHALL позволять операции над этим списком:
|
||
выбрать кандидата базы, переключиться на другого кандидата и снять матч с базы
|
||
обратно на нейронку («без базы»). Список источников SHALL показываться только
|
||
при наличии плана распознавания.
|
||
|
||
#### Scenario: Нейронка — строка в общем списке
|
||
|
||
- **GIVEN** загрузка в `review` с распознаванием нейронкой и одним или
|
||
несколькими кандидатами метабаз
|
||
- **WHEN** пользователь открывает `GET /review/{id}`
|
||
- **THEN** источники показаны единым списком, где строка «распознано
|
||
нейронкой» стоит наравне с кандидатами баз
|
||
- **AND** активным отмечен ровно один источник (текущий эффективный матч)
|
||
|
||
#### Scenario: Выбор кандидата одним кликом
|
||
|
||
- **GIVEN** на экране ревью выбран один кандидат метабазы
|
||
- **WHEN** пользователь кликает/тапает строку другого кандидата
|
||
- **THEN** выбранный кандидат сохраняется активным, прочие — неактивны, без
|
||
отдельного нажатия кнопки «выбрать»
|
||
- **AND** инфо-часть и предпросмотр раскладки сразу обновляются под выбранного
|
||
кандидата без полной перезагрузки страницы
|
||
- **AND** панель действий обновляется тем же ответом (out-of-band): доступность
|
||
«Применить» синхронна наличию предпросмотра раскладки
|
||
|
||
#### Scenario: Снятие матча в пользу нейронки
|
||
|
||
- **GIVEN** на экране ревью активен кандидат метабазы с названием «Fargo»
|
||
- **WHEN** пользователь кликает строку «распознано нейронкой»
|
||
- **THEN** матч с базой снимается (источник — нейронка, «без базы»), тег
|
||
папки провайдера не проставляется
|
||
- **AND** поля источника — из распознавания нейронкой, без унаследованных
|
||
от прежнего кандидата название/год
|
||
|
||
### Requirement: Ручное добавление источника по id или URL
|
||
|
||
Когда автопоиск по базам промахнулся, экран ревью SHALL позволять добавить
|
||
источник вручную — по идентификатору записи метабазы или, где применимо, по
|
||
её URL. Ввод SHALL разбираться и валидироваться в пару
|
||
`(provider, provider_id)` на входной границе (`internal/httpapi`); допустимые
|
||
провайдеры — `tmdb`, `tvdb`, `imdb`. Добавленный источник SHALL появляться в
|
||
списке как выбираемая строка; при совпадении `provider:id` с уже присутствующим
|
||
источником новая строка NOT создаётся, а выбирается существующая.
|
||
Некорректный ввод SHALL отклоняться с сообщением, не меняя текущий активный
|
||
источник.
|
||
|
||
#### Scenario: Добавление кандидата по URL TMDB
|
||
|
||
- **GIVEN** загрузка в `review`, где нужной записи нет среди автокандидатов
|
||
- **WHEN** пользователь вводит URL записи TMDB и подтверждает добавление
|
||
- **THEN** из URL извлекаются провайдер и id, источник добавляется в список
|
||
выбираемой строкой
|
||
|
||
#### Scenario: Дубль id выбирает существующую строку
|
||
|
||
- **GIVEN** в списке уже есть кандидат с данным `provider:id`
|
||
- **WHEN** пользователь добавляет вручную тот же `provider:id`
|
||
- **THEN** новая строка не создаётся, активным становится существующий
|
||
кандидат
|
||
|
||
#### Scenario: Некорректный ввод отклонён
|
||
|
||
- **WHEN** пользователь вводит нераспознаваемый id/URL
|
||
- **THEN** экран показывает сообщение об ошибке и не меняет текущий активный
|
||
источник
|
||
|
||
### Requirement: Подтверждение матча обновляет отображаемое имя
|
||
|
||
Система SHALL при подтверждении матча в ревью запускать обновление отображаемого
|
||
имени загрузки по подтверждённому распознаванию (см. capability `ingest`):
|
||
переливать **полный ярлык** имени — «Название (режиссёр, год)», для сериала со
|
||
сводкой сезонов — в `download.display_name` и в имя раздачи qBittorrent, без
|
||
нового вызова LLM. Имя строится из эффективных полей (override → распознавание с
|
||
вложенным матчем → сохранённый контекст). Подтверждением матча SHALL
|
||
считаться как выбор кандидата из списка совпадений, так и ручное добавление
|
||
источника по id/URL (оба закрепляют провайдера и каноническое название).
|
||
|
||
При закреплении выбранного/добавленного источника система SHALL best-effort
|
||
получить режиссёра этого источника из метабазы (credits по `provider:id`, см.
|
||
`metadata-match`) и закрепить его как override, чтобы он попал в эффективные поля
|
||
и в ярлык. Недоступность credits или отсутствие режиссёра SHALL NOT проваливать
|
||
выбор источника: режиссёр остаётся из более низкого слоя (сохранённый контекст)
|
||
или пустым. Так режиссёр из метабазы появляется и на **основном** пути
|
||
подтверждения — ручном выборе кандидата, а не только при авто-матче.
|
||
|
||
Обновление SHALL выполняться после успешного закрепления выбора кандидата и
|
||
SHALL быть best-effort по отношению к qBittorrent: недоступность клиента SHALL
|
||
NOT проваливать команду ревью. Это согласуется с инвариантом «авто-действие
|
||
только при подтверждённом матче».
|
||
|
||
#### Scenario: Выбор кандидата обновляет имя
|
||
|
||
- **GIVEN** загрузка в ревью с пустым или неинформативным `display_name`
|
||
(например, «Unknown») и списком кандидатов
|
||
- **WHEN** пользователь выбирает кандидата, подтверждая матч
|
||
- **THEN** выбор кандидата закрепляется как и прежде
|
||
- **AND** `download.display_name` обновляется полным ярлыком
|
||
«Название (режиссёр, год)» (для сериала — со сводкой сезонов)
|
||
- **AND** раздача в qBittorrent переименовывается в то же имя
|
||
|
||
#### Scenario: Ручное добавление источника обновляет имя
|
||
|
||
- **GIVEN** загрузка в ревью без совпадений в списке
|
||
- **WHEN** пользователь вручную добавляет источник по id/URL, подтверждая матч
|
||
- **THEN** источник закрепляется как и прежде
|
||
- **AND** `download.display_name` и имя раздачи в qBittorrent обновляются
|
||
полным ярлыком подтверждённого источника
|
||
|
||
#### Scenario: Выбор кандидата подтягивает режиссёра в ярлык
|
||
|
||
- **GIVEN** загрузка в ревью, у выбранного кандидата в credits метабазы указан
|
||
режиссёр
|
||
- **WHEN** пользователь выбирает кандидата, подтверждая матч
|
||
- **THEN** режиссёр best-effort извлекается из метабазы и закрепляется override
|
||
- **AND** `download.display_name` получает полный ярлык с этим режиссёром
|
||
|
||
#### Scenario: Режиссёр кандидата недоступен — выбор не ломается
|
||
|
||
- **GIVEN** выбор кандидата, для которого credits недоступны или режиссёра нет
|
||
- **WHEN** пользователь подтверждает матч
|
||
- **THEN** выбор источника выполнен, режиссёр берётся из сохранённого контекста
|
||
или остаётся пустым
|
||
- **AND** команда ревью не возвращает ошибку
|
||
|
||
#### Scenario: Недоступность qBittorrent не ломает выбор кандидата
|
||
|
||
- **GIVEN** выбор кандидата в ревью
|
||
- **WHEN** переименование раздачи в qBittorrent завершается ошибкой
|
||
- **THEN** выбор кандидата и обновление `download.display_name` выполнены
|
||
- **AND** команда ревью не возвращает ошибку
|
||
|
||
### Requirement: Инфо и предпросмотр выбранного источника
|
||
|
||
В едином блоке выбора источника экран ревью SHALL показывать для **выбранного
|
||
(активного)** источника две части: **инфо** — тип (read-only, movie/series),
|
||
название, оригинальное название, год, режиссёра эффективного источника,
|
||
разрешённого слоями (`override`/подтверждённый матч+кандидат → сохранённый при
|
||
приёме контекст раздачи, `parsed_context`; когда режиссёр недоступен ни в одном
|
||
слое — пусто/прочерк, не ломая вёрстку), для сериала — сводку сезонов (один
|
||
сезон, диапазон/список для многосезонного пака или «Спецвыпуски»); и
|
||
**предпросмотр раскладки** — целевые пути хардлинков этого источника. Обе части
|
||
SHALL относиться именно к активному источнику и SHALL обновляться при смене
|
||
выбора. Отрисовка блока (показ инфо и предпросмотра) MUST NOT создавать
|
||
хардлинки: раскладка создаётся только явным действием «Применить». Совпадение
|
||
целевых путей предпросмотра с результатом применения регулируется требованием
|
||
«Превью раскладки через единую логику именования» (`web-ui`).
|
||
|
||
#### Scenario: Инфо и предпросмотр относятся к активному источнику
|
||
|
||
- **GIVEN** в списке активен кандидат метабазы
|
||
- **WHEN** пользователь смотрит инфо-часть и предпросмотр раскладки
|
||
- **THEN** показаны тип, название, ориг. название, год (и сводка сезонов для
|
||
сериала) именно этого источника и предпросмотр его целевых путей
|
||
|
||
#### Scenario: Просмотр блока не создаёт раскладку
|
||
|
||
- **GIVEN** экран ревью с показанным блоком выбора источника
|
||
- **WHEN** пользователь только просматривает инфо и предпросмотр, не нажимая
|
||
«Применить»
|
||
- **THEN** хардлинки не создаются, файлы под `paths.movies`/`series` не
|
||
меняются
|
||
|
||
#### Scenario: Режиссёр показан, когда доступен
|
||
|
||
- **GIVEN** активный источник — подтверждённый матч, несущий режиссёра
|
||
- **WHEN** отображается инфо-часть выбранного источника
|
||
- **THEN** в ней показан режиссёр этого источника
|
||
- **AND** при отсутствии режиссёра во всех слоях место остаётся пустым (или
|
||
прочерком), не ломая вёрстку
|
||
|
||
#### Scenario: Режиссёр берётся из контекста, когда матч его не даёт
|
||
|
||
- **GIVEN** активный источник без режиссёра в плане, но с режиссёром в
|
||
сохранённом контексте (`parsed_context`)
|
||
- **WHEN** отображается инфо-часть выбранного источника
|
||
- **THEN** в ней показан режиссёр из контекста (нижний слой разрешения)
|
||
|
||
### Requirement: Разделение труда транспортов в ревью
|
||
|
||
Состояние ревью SHALL быть единым (в SQLite) для всех транспортов. Веб-UI SHALL
|
||
быть поверхностью точных правок (маппинг файлов, ручной ввод/выбор источника по
|
||
id или URL, «без базы», предпросмотр). Telegram SHALL давать быстрые действия
|
||
(одобрить, подсказать, **быстрый выбор источника из готового списка кандидатов
|
||
метабазы**, «Позже»/«Отклонить») и эскалацию в веб deep-link'ом на ту же
|
||
страницу; точечные правки, не помещающиеся в чат (ручной ввод id/URL, маппинг
|
||
файлов), SHALL делаться в вебе.
|
||
|
||
#### Scenario: Эскалация из Telegram в веб
|
||
|
||
- **GIVEN** загрузка в `review`, требующая точечного маппинга файлов
|
||
- **WHEN** пользователь в Telegram выбирает «В вебе»
|
||
- **THEN** бот даёт deep-link на страницу ревью той же загрузки
|
||
|
||
#### Scenario: Быстрый выбор кандидата в Telegram, точный ввод — в вебе
|
||
|
||
- **GIVEN** загрузка в `review` с сохранёнными кандидатами метабазы
|
||
- **WHEN** пользователь выбирает кандидата inline-кнопкой в Telegram
|
||
- **THEN** кандидат закрепляется как источник (тот же единый выбор источника, что
|
||
и в вебе), а ручной ввод id/URL и «без базы» остаются точными правками веба
|
||
|
||
### Requirement: Петлевые действия ревью обновляют экран на месте
|
||
|
||
Петлевые действия распознавания на экране ревью — **Распознать заново** (`rerecognize`) и **Уточнить** (`refine`) — SHALL выполняться htmx-запросом и обновлять тело экрана ревью на месте (partial swap), без полной перезагрузки страницы и без сброса позиции прокрутки. Поскольку эти действия асинхронны (переводят загрузку в `recognizing`, распознавание доделывает воркер), своп SHALL отражать актуальное состояние — состояние `recognizing` с индикацией «идёт распознавание», а не мгновенно готовый план. Накопленные подсказки и ручные override MUST переживать перераспознавание. Это согласуется с уже действующим частичным свопом при смене выбранного источника (см. «Единый список источников совпадения на ревью»).
|
||
|
||
Пока загрузка в `recognizing`, экран ревью SHALL сам обновляться поллингом
|
||
htmx-фрагмента (`GET /fragments/downloads/{id}/review`) и по завершении
|
||
распознавания SHALL автоматически смениться на готовый план (список источников,
|
||
инфо и предпросмотр активного источника), без ручного обновления страницы. Как
|
||
только состояние вышло из `recognizing`, фрагмент SHALL возвращаться без
|
||
поллера, и опрос прекращается. Без htmx экран SHALL деградировать до ручной
|
||
ссылки «Обновить».
|
||
|
||
Выходы из ревью, после которых загрузка покидает `review` — **Применить**
|
||
(`apply` → раскладка/`done`), **Позже** (`defer` → `deferred`) и **Отклонить**
|
||
(`cancel` → `cancelled`), — НЕ обязаны свопить экран на месте и MAY уводить с
|
||
экрана ревью навигацией (редирект/`HX-Redirect`), поскольку загрузка перестаёт
|
||
быть предметом этого экрана.
|
||
|
||
Поведение петлевых действий MUST деградировать без htmx: без заголовка
|
||
`HX-Request` обработчик SHALL исполнять то же доменное действие и отвечать
|
||
редиректом на `/review/{id}`, как раньше.
|
||
|
||
#### Scenario: Перераспознавание свопит экран в состояние recognizing
|
||
|
||
- **GIVEN** загрузка в `review`, экран ревью открыт
|
||
- **WHEN** пользователь нажимает «Распознать заново» или «Уточнить» с подсказкой
|
||
(htmx активен)
|
||
- **THEN** тело экрана ревью обновляется на месте в состояние `recognizing` с
|
||
индикацией «идёт распознавание», без полной перезагрузки и без прыжка
|
||
прокрутки наверх
|
||
- **AND** накопленные подсказки и ручные override сохраняются
|
||
|
||
#### Scenario: Экран сам обновляется до готового плана
|
||
|
||
- **GIVEN** экран ревью показывает состояние `recognizing` после петлевого
|
||
действия
|
||
- **WHEN** воркер завершает распознавание и загрузка снова в `review`
|
||
- **THEN** экран автоматически (поллингом фрагмента) сменяется на готовый план
|
||
(источники, инфо, предпросмотр), без ручного обновления
|
||
- **AND** после выхода из `recognizing` фрагмент возвращается без поллера и опрос
|
||
прекращается
|
||
|
||
#### Scenario: Выход из ревью уводит с экрана
|
||
|
||
- **GIVEN** загрузка в `review` с готовым превью
|
||
- **WHEN** пользователь нажимает «Применить», «Позже» или «Отклонить»
|
||
- **THEN** загрузка покидает `review` (соответственно `done`/`deferred`/
|
||
`cancelled`), а интерфейс уводит пользователя с экрана ревью навигацией
|
||
|
||
#### Scenario: Деградация петлевого действия без htmx
|
||
|
||
- **WHEN** «Распознать заново» или «Уточнить» приходит POST-запросом без
|
||
заголовка `HX-Request`
|
||
- **THEN** обработчик исполняет то же доменное действие и отвечает редиректом на
|
||
`/review/{id}`, поведение без JavaScript не ломается
|
||
|