Files
avandClaude Opus 4.8 1d6f8f3449 review: убрать явный переключатель типа movie↔series из всех транспортов
Кнопка «Тип» была только в 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>
2026-07-18 16:50:43 +03:00

34 KiB
Raw Permalink Blame History

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), Позже (deferdeferred) и Отклонить (cancelcancelled), — НЕ обязаны свопить экран на месте и 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 не ломается