На /download/{id} поле «Режиссёр» было захардкожено прочерком, хотя экран
ревью режиссёра уже выводит: слоистое разрешение полей ярлыка было заперто в
неэкспортируемом worker.effectiveDisplayName. Из-за этого билдеры вью видели
только слой распознавания+матч (rd.Plan.Director) без слоя контекста — то же
на экране ревью.
Вынес разрешение в экспортируемую naming.EffectiveFields(parsedContext, plan)
LabelFields с методом Label(): выбор слоя по сырым значениям (как прежде),
выбранные скаляры возвращаются очищенными (sanitize идемпотентен, display_name
побайтно тот же). effectiveDisplayName стал тонкой обёрткой; страница загрузки
и экран ревью берут режиссёра из той же функции — согласованно с заголовком.
OpenSpec: web-ui (ADDED «Режиссёр в блоке распознавания страницы загрузки»),
review (MODIFIED «Инфо и предпросмотр выбранного источника» — слоистое
разрешение с фолбэком на контекст). Change заархивирован. Беклог: закрыта
rezhisser-v-kartochke-zagruzki.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31 KiB
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 → перераспознавание с ручным
подтверждением). Экран ревью MUST NOT содержать команду переключения типа
movie↔series: тип показывается read-only, а его корректировка выполняется
мягкой подсказкой через Уточнить. Команды из любого транспорта SHALL
сериализоваться worker'ом под единой блокировкой; применяется последняя валидная
команда.
Команды, которым нужен источник (Применить, Уточнить, Распознать
заново, Привязать заново, а также фиксация типа), 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 пользователь открывает экран ревью
- THEN отдельной команды/кнопки переключения movie↔series на экране нет
- AND тип показан read-only в инфо-части выбранного источника
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 пользователь зафиксировал тип
seriesкак override - WHEN запускается перераспознавание по новой подсказке
- THEN в новом эффективном плане тип остаётся
series
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 быть поверхностью точных правок (маппинг файлов, выбор/ввод источника, предпросмотр). Telegram SHALL давать быстрые действия (одобрить, подсказать, переключить тип, «Позже»/«Отклонить») и эскалацию в веб deep-link'ом на ту же страницу; точечные правки, не помещающиеся в чат, SHALL делаться в вебе.
Scenario: Эскалация из Telegram в веб
- GIVEN загрузка в
review, требующая точечного маппинга файлов - WHEN пользователь в Telegram выбирает «В вебе»
- THEN бот даёт deep-link на страницу ревью той же загрузки
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 не ломается