# review Specification ## Purpose Ревью раскладки человеком после распознавания и матча: петля «догадка → подсказка → перераспознавание», команды (Применить/Уточнить/Распознать заново/ Тип/Игнор/Позже/Отклонить/Undo/Привязать заново), мягкие подсказки vs жёсткие `override`, единый список источников совпадения с ручным добавлением и предпросмотром (превью = применение), разделение труда транспортов (веб — точные правки, Telegram — быстрые действия и эскалация в веб). ## Requirements ### Requirement: Вход в review с явной причиной Когда модель уверенности не разрешает авто-раскладку, система SHALL переводить загрузку в `review` и SHALL показывать **конкретную причину** (низкая самооценка LLM; нет матча в базе или несколько кандидатов; предупреждение структурной валидации; неразобранный ответ LLM), а не обобщённое «не уверен». Поверхность решения SHALL быть единой для всех транспортов и содержать источник (имя, контекст, дерево файлов), догадку системы (тип, название, год, матч) и превью целевой раскладки. #### Scenario: Причина видна в интерфейсе - **GIVEN** загрузка ушла в `review` из-за отсутствия матча в базе - **WHEN** пользователь открывает ревью - **THEN** показана конкретная причина (напр. «нет в TMDB · уверенность 0.46») ### Requirement: Команды ревью и их эффекты Экран ревью SHALL предоставлять команды: **Применить** (создать хардлинки по эффективному плану), **Уточнить** (добавить подсказку → перераспознать), **Распознать заново** (повторный прогон без новой подсказки), **Тип** (переключить movie↔series), **Игнор файла**, **Позже** (`deferred`), **Отклонить** (`cancelled`), **Undo** (снять созданные ссылки → `reverted`) и **Привязать заново** (из `reverted`/`cancelled`/`target_missing` → перераспознавание с ручным подтверждением). Команды из любого транспорта SHALL сериализоваться worker'ом под единой блокировкой; применяется последняя валидная команда. Команды, которым нужен источник, SHALL проверять его наличие синхронно перед действием. #### Scenario: Применение создаёт раскладку - **GIVEN** загрузка в `review` с эффективным планом - **WHEN** пользователь выбирает «Применить» - **THEN** создаются хардлинки по плану, задача переходит к раскладке #### Scenario: Отклонить и привязать заново - **GIVEN** загрузка в `review` - **WHEN** пользователь «Отклонить», затем «Привязать заново» - **THEN** задача уходит в `cancelled`, а затем снова на распознавание с ручным подтверждением (авто-раскладка не делается) ### 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 выполняться через раундтрип на сервер (форма/htmx), без клиентского пересчёта доменного состояния. Список источников SHALL показываться только при наличии плана распознавания. #### Scenario: Нейронка — строка в общем списке - **GIVEN** загрузка в `review` с распознаванием нейронкой и одним или несколькими кандидатами метабаз - **WHEN** пользователь открывает `GET /review/{id}` - **THEN** источники показаны единым списком, где строка «распознано нейронкой» стоит наравне с кандидатами баз - **AND** активным отмечен ровно один источник (текущий эффективный матч) #### Scenario: Переключение между кандидатами - **GIVEN** на экране ревью выбран один кандидат метабазы - **WHEN** пользователь выбирает другого кандидата из списка - **THEN** активным становится выбранный кандидат, прочие — неактивны #### 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 показывать для рассматриваемого источника (нейронка, кандидат базы или добавленный вручную) **поля** результата — тип, название, год, с зарезервированным местом под режиссёра. Показ полей источника MUST NOT менять сохранённый матч загрузки и MUST NOT создавать хардлинки: сохранённый матч меняется только явным выбором источника, а раскладка — только действием «Применить». Совпадение целевых путей предпросмотра с результатом применения регулируется требованием «Превью раскладки через единую логику именования» (`web-ui`). #### Scenario: Предпросмотр полей без фиксации выбора - **GIVEN** список источников на экране ревью - **WHEN** пользователь рассматривает источник, ещё не выбрав его активным - **THEN** показаны поля результата (тип, название, год) для этого источника - **AND** сохранённый матч загрузки не меняется, хардлинки не создаются #### Scenario: Зарезервированное место под режиссёра - **GIVEN** режиссёр из метабазы пока не загружается - **WHEN** отображается предпросмотр полей источника - **THEN** в предпросмотре присутствует место под режиссёра, показанное пустым (или прочерком), не ломая вёрстку ### Requirement: Разделение труда транспортов в ревью Состояние ревью SHALL быть единым (в SQLite) для всех транспортов. Веб-UI SHALL быть поверхностью точных правок (маппинг файлов, выбор/ввод источника, предпросмотр). Telegram SHALL давать быстрые действия (одобрить, подсказать, переключить тип, «Позже»/«Отклонить») и эскалацию в веб deep-link'ом на ту же страницу; точечные правки, не помещающиеся в чат, SHALL делаться в вебе. #### Scenario: Эскалация из Telegram в веб - **GIVEN** загрузка в `review`, требующая точечного маппинга файлов - **WHEN** пользователь в Telegram выбирает «В вебе» - **THEN** бот даёт deep-link на страницу ревью той же загрузки