From e7fe88a986aa4c1fa24fb726c4807eb466acb0f5 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Fri, 3 Jul 2026 09:52:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D1=91=D0=BB=20change=20revie?= =?UTF-8?q?w-source-selection:=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D0=B4=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE?= =?UTF-8?q?=D1=82=D1=80=20=D0=B2=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20(opens?= =?UTF-8?q?pec)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переработка экрана ревью: единый список источников (нейронка наравне с кандидатами баз), выбор/переключение/снятие в пользу нейронки, ручное добавление по id/URL, предпросмотр полей и целевых путей до применения. Дизайн отревьюен: единая деривация «источник → overrides» (preview==apply, чинит залипший override title/year). Ограничились существующими capabilities. Беклог: добавил две идеи — «Пересмотр набора capabilities и рефакторинг спек» и «Сила совпадения кандидата / пересмотр распознавания и матчинга». Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/backlog.md | 45 ++++ .../review-source-selection/.openspec.yaml | 2 + .../changes/review-source-selection/design.md | 205 ++++++++++++++++++ .../review-source-selection/proposal.md | 87 ++++++++ .../specs/web-ui/spec.md | 144 ++++++++++++ .../changes/review-source-selection/tasks.md | 61 ++++++ 6 files changed, 544 insertions(+) create mode 100644 openspec/changes/review-source-selection/.openspec.yaml create mode 100644 openspec/changes/review-source-selection/design.md create mode 100644 openspec/changes/review-source-selection/proposal.md create mode 100644 openspec/changes/review-source-selection/specs/web-ui/spec.md create mode 100644 openspec/changes/review-source-selection/tasks.md diff --git a/docs/backlog.md b/docs/backlog.md index 774b757..569ccef 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -191,6 +191,51 @@ qBittorrent, пул LLM-вызовов и запись в SQLite спроект [docs/conventions](conventions/README.md), [«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language). +### Пересмотр набора capabilities и рефакторинг спек + +Деление capabilities в OpenSpec сложилось по ходу миграции и смешивает +разные действия в одной спеке. Пример: `recognition` держит и разбор через +LLM, и **сверку с внешними базами** — а «поиск в базе» и «подтверждение +матча официальным id» суть разные действия, значит и разные capability. +Нужно пересмотреть набор и границы, чтобы имя capability отвечало одному +поведению: + +- `recognition` — только разбор сигналов через LLM (план, тип, название, + файлы → серии); +- отдельные capability под работу с метабазами: поиск записей во внешних + базах и сверку/подтверждение матча (разнести пока смешанное в + `recognition`); +- `web-ui` — только общее оформление, дизайн-система и общие компоненты + страниц; +- `review` — весь процесс ревью после распознавания и матча (мигрировать из + [review-ux.md](specs/review-ux.md); сейчас поведение ревью не в OpenSpec). + +Работа чисто по спекам (границы, RENAMED/MOVED requirements), код не +трогаем. Ценно тем, что снимает путаницу «какой capability трогать» на +каждой задаче. + +Связано: `CLAUDE.md` (SDD, миграция capabilities), openspec/specs +(`recognition`, `web-ui`), [review-ux.md](specs/review-ux.md), +[«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language). + +### Сила совпадения кандидата и пересмотр распознавания/матчинга _(идея)_ + +Сейчас у кандидата метабазы нет метрики силы совпадения (`metadata_candidate` +хранит provider/id/title/year/url), а решение «авто vs review» — по правилу +«единственный сильный матч + валидация», не по числовой уверенности. Для +ревью это значит: список кандидатов нечем отсортировать/подсветить по +уверенности — берём порядок сбора. Идея — ввести на этапе матча **силу +совпадения кандидата** (точное совпадение названия+года vs частичное) для +сортировки и подсказки в UI. Шире — отдельно продумать **сам процесс +распознавания и матчинга**: границы «разбор LLM / поиск в базе / сверка», +что храним у кандидата, как считаем и показываем уверенность. Требует +проработки перед реализацией. + +Связано: [recognition.md](specs/recognition.md) (модель уверенности), +[ADR-2026-06-13-auto-link-requires-db-match](adr/ADR-2026-06-13-auto-link-requires-db-match.md), +[«Ревью: выбор источника совпадения»](#ревью-выбор-источника-совпадения-и-предпросмотр), +[«Пересмотр набора capabilities»](#пересмотр-набора-capabilities-и-рефакторинг-спек). + ### История переходов загрузки Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто diff --git a/openspec/changes/review-source-selection/.openspec.yaml b/openspec/changes/review-source-selection/.openspec.yaml new file mode 100644 index 0000000..43e65ca --- /dev/null +++ b/openspec/changes/review-source-selection/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/review-source-selection/design.md b/openspec/changes/review-source-selection/design.md new file mode 100644 index 0000000..1ea50d1 --- /dev/null +++ b/openspec/changes/review-source-selection/design.md @@ -0,0 +1,205 @@ +## Context + +Экран ревью уже умеет много: выбор кандидата (`ChooseCandidate`), ручной +ввод id (`SetProviderID`), «без базы» (`ClearProvider`), превью текущего +плана (`ReviewData.Preview` через `layout.BuildLinks`). Механика выбора +источника — это **overrides**: выбор кандидата пиннит `provider`, +`provider_id` и (если есть) `title`/`year`, после чего эффективный план +пересобирается `applyOverrides` и печатается превью путей. + +Проблемы текущего экрана — не в отсутствии операций, а в подаче: + +- нейронка и кандидаты баз показаны как разные сущности (план сверху, + кандидаты снизу; «без базы» — особое состояние); +- **превью есть только для уже выбранного** источника: чтобы увидеть пути + для другого кандидата, его надо сначала выбрать (запись пиннится), т.е. + «примерить вслепую». + +Ограничение capability `web-ui`: клиент не пересчитывает доменное +состояние — все доменные величины (пути, поля) приходят с сервера +(требование «Клиентские взаимодействия без сборки»). + +## Goals / Non-Goals + +**Goals:** + +- Единый список источников: нейронка + кандидаты баз + добавленные вручную, + один активный. +- Предпросмотр полей (тип/название/год) и целевых путей **для любого + источника до его выбора**, посчитанный на сервере. +- Ручное добавление источника по id/URL как строки списка. +- Переиспользовать существующую логику (`applyOverrides` + + `layout.BuildLinks`), не дублируя правила именования. + +**Non-Goals:** + +- Fetch деталей записи из метабазы (режиссёр и пр.) — только резервируем + место в UI. +- **Сила совпадения кандидата** (per-candidate score) и сортировка/подсветка + списка по ней — сегодня у кандидата такого поля нет; список берём в + порядке сбора. Отдельная идея беклога (там же — пересмотр процесса + распознавания/матчинга). +- Изменение решения «авто vs review» и модели уверенности — не трогаем: авто + по-прежнему только при единственном сильном матче + валидации, иначе + review; фича работает **внутри** уже наступившего review. +- Отдельный capability `review` и перекройка домена метабаз — отдельная + задача беклога. +- Редактор маппинга «файл → серия» и правки в Telegram. + +## Decisions + +### 0. Двухуровневая модель ревью сохраняется, нового подтверждения нет + +В ревью два независимых шага, оба остаются как есть: + +1. **Выбор источника** — кнопки «выбрать» / «Задать id» / «Без базы» (POST → + `ChooseCandidate`/`SetProviderID`/`ClearProvider`) **фиксируют** матч, + записывая overrides. Файлы не раскладываются, лишь пересобирается план. +2. **«Применить»** (POST → `Apply`) — единственный шаг, создающий хардлинки. + +Фича добавляет **только предпросмотр до шага 1**: увидеть поля и целевые +пути каждого источника, **не нажимая «выбрать»** (не трогая БД). Семантику +«выбрать» и «Применить» не меняем, новых кнопок-подтверждений не вводим. + +### 1. Предпросмотр считается на сервере эфемерно, без записи overrides + +Добавляем в `worker` чистый расчёт: по текущему плану и **гипотетическому** +источнику `(provider, provider_id, опц. title/year)` собрать эффективные +поля и `[]layout.Link`, **не записывая** overrides в БД. Технически — тот +же `applyOverrides` + `layouter.BuildLinks(toLayoutPlan(...))`, что и в +`ReviewData`, но поверх копии плана с временными пинами источника; в БД +ничего не пишем. + +`ReviewData` расширяется срезом «источник → (поля, превью путей, активен +ли)» для нейронки и каждого кандидата. Транспорт рендерит их строками +списка. + +- **Почему так, а не «выбрать → посмотреть → отменить»:** выбор сейчас + пиннит запись (пишет overrides) и меняет сохранённый матч; «примерка» + не должна трогать состояние (инвариант «предпросмотр ничего не + раскладывает и не фиксирует»). +- **Альтернатива — отдельный эндпоинт `GET /review/{id}/preview?source=…`, + отдающий htmx-фрагмент:** отвергнута для известных источников — превью + считается быстро и без сети (`BuildLinks` — чистая работа с путями), их + дешевле посчитать сразу и вложить в страницу; лишний раундтрип на каждое + наведение не нужен. + +### 1a. Единая деривация «источник → набор overrides» (гарантия preview == apply) + +**Проблема, найденная на ревью дизайна.** Пины `ovrTitle`/`ovrYear` пишутся +`SetOverride` и не удаляются (в store нет `DeleteOverride`). `ChooseCandidate` +пиннит title/year только при непустых полях кандидата (review.go:573-579), а +`ClearProvider` их вообще не трогает (review.go:624-640). Значит после выбора +кандидата «Fargo/2014» и последующего переключения на нейронку или на ручной +кандидат без title запиненный `ovrTitle=Fargo` **остаётся**. Тогда `applyOverrides` +(review.go:742) при `Apply` возьмёт `Fargo`, а эфемерное превью источника без +собственного title покажет title из плана → **пути превью ≠ пути применения**. +Это и латентный баг текущего кода (переключение кандидатов тянет чужой title). + +**Решение.** Ввести одну чистую функцию «источник → полный самосогласованный +набор overrides» и использовать её **и в превью, и в коммите**: + +- источник-кандидат с title/year → пиннит их; **без** title/year → пишет + **пустую строку** в `ovrTitle`/`ovrYear` (в `applyOverrides` пустая строка + трактуется как «нет override», review.go:742,745 — `DeleteOverride` не нужен, + берётся значение плана); +- источник-нейронка (`ClearProvider`) → `provider=none` и пустые + `ovrTitle`/`ovrYear` (поля из плана распознавания). + +Так каждый источник даёт детерминированный эффективный план; превью считается +тем же набором overrides, что запишет выбор → **preview == apply по построению**. + +- **Затрагивает поведение `worker`** (`ChooseCandidate`/`ClearProvider` теперь + очищают title/year), а не только web-ui. Это осознанно: спека остаётся в + `web-ui` (наблюдаемое — «превью == применение» и «переключение не тянет чужие + поля»), а правка команд ревью — реализация. Заодно чиним латентный баг. +- **Альтернатива — превью «симулирует» унаследованные пины** (показывать чужой + title у нейронки): отвергнута — противоречит принципу «нейронка = поля + распознавания» и путает пользователя. + +### 2. Клиент только показывает предпосчитанное, не считает пути + +Превью всех известных источников кладём в страницу (data-блоки/скрытые +секции), клиентский vanilla-JS лишь переключает видимость по выбору строки. +Никакого доменного пересчёта на клиенте — соблюдаем требование `web-ui`. +Фактическая смена активного источника (пиннинг) — по явному действию формой +(раундтрип), как сейчас. + +### 3. Нейронка — синтетическая строка, а не особый режим + +Строку «распознано нейронкой» синтезируем из сырого плана распознавания +(`provider = none`): её поля — то, что дал LLM без базы, её превью — +раскладка без тега провайдера. Выбор этой строки = существующий +`ClearProvider`. Так «без базы» перестаёт быть отдельным состоянием UI и +становится обычной строкой списка. + +### 4. Ручной источник — кандидат в том же списке + +Ручной ввод (id или URL) на входной границе `httpapi` парсим в +`(provider, provider_id)`. Допустимые провайдеры — `tmdb`, `tvdb`, `imdb` +(набор согласован с `providerTag`/`providerURL`; `tvmaze` — только источник +автопоиска, вручную не вводится). Добавляем как **строку списка**: сохраняем +`metadata_candidate` с этим `provider`/`provider_id` и (если дан) `url`; +`title`/`year` — пустые (деталей не тянем). Дедуп по `provider:id`: если такой +источник уже в списке — не плодим строку, а выбираем существующую. Такой +кандидат участвует в выборе и предпросмотре наравне с автонайденными; выбор — +тот же `ChooseCandidate` (с очисткой title/year по решению 1a). + +**Парсинг URL реалистичен не для всех баз.** `providerURL` строит TVDB как +`thetvdb.com/dereferrer/series/{numeric_id}`, но с сайта пользователь копирует +`thetvdb.com/series/{slug}` — без числового id. Поэтому обещаем: **URL — для +TMDB/IMDb**, для **TVDB — ручной ввод числового id** (slug из URL не +распознаём). Список принимаемых паттернов фиксируем в реализации как обратный +к `providerURL`. + +- Превью ручного источника корректно и без title/year из базы: имя папки + берётся из title **плана**, а от источника меняется лишь тег провайдера + в пути. Значит «предпросмотр путей» для ручного кандидата полноценен. +- **Альтернатива — просто пиннить `SetProviderID` без строки в списке:** + отвергнута — тогда ручной источник не «переключаемый» наравне с + остальными, что противоречит принципу единого списка. + +### 5. Режиссёр — зарезервированное место, источник позже + +В предпросмотре полей выводим строку «Режиссёр» пустой (прочерк). Fetch +деталей из метабазы — отдельная задача; при появлении источника (детали по +id или парсинг из контекста загрузки) заполняем это же место. + +## Risks / Trade-offs + +- **[Рассинхрон превью и применения]** Превью и реальная раскладка должны + идти одной логикой. → Оба используют `layout.BuildLinks`/`naming` и **одну + деривацию «источник → overrides»** (решение 1a); в тестах проверяем равенство + путей превью и применения (требование `web-ui` «Превью совпадает с реальной + раскладкой», сценарий «Переключение источника не тянет чужие поля»). +- **[Залипший override title/year]** Пины title/year не удаляются и могут + утечь между источниками (латентный баг). → Решение 1a: выбор источника пишет + полный самосогласованный набор (пустая строка = сброс к плану). +- **[Раздувание страницы]** Предпосчёт превью для всех кандидатов кладёт N + наборов путей в HTML. → Кандидатов немного (потолок сбора уже есть в + `recognition`); `BuildLinks` без сети. Приемлемо; если станет тяжело — + ленивый htmx-фрагмент (решение 1, альтернатива) как эволюция. +- **[Ручной кандидат с пустыми title/year]** Строки списка и матч-ссылка + должны переживать пустые поля. → `matchURL` уже строит URL из + provider/id; заголовок берём из плана. Проверить рендер строки без + title/year. +- **[Дубль ручного и автокандидата]** Пользователь может ввести id, уже + присутствующий в списке. → Дедуп по `provider:id` при добавлении (как в + сборе кандидатов): не плодим строку, просто выбираем существующую. + +## Migration Plan + +- Данные: миграций схемы не требуется — ручной кандидат ложится в + существующую `metadata_candidate` (title/year nullable уже так). Новый + источник строки — пользовательское действие, обратная совместимость + полная. +- Откат: изменения ограничены страницей ревью и добавочным методом + предпросмотра в `worker`; откат — возврат прежнего шаблона/обработчика, + данные не затрагиваются. + +## Open Questions + +Блокирующих открытых вопросов нет. Оставшийся детерминированный на +реализацию пункт — точный список принимаемых URL-паттернов ручного ввода +(обратный к `providerURL`: `themoviedb.org/{movie,tv}/{id}`, +`imdb.com/title/{id}`; TVDB — числовой id, не slug), см. решение 4. diff --git a/openspec/changes/review-source-selection/proposal.md b/openspec/changes/review-source-selection/proposal.md new file mode 100644 index 0000000..741fada --- /dev/null +++ b/openspec/changes/review-source-selection/proposal.md @@ -0,0 +1,87 @@ +## Why + +Экран ревью сегодня трактует распознавание нейронкой и совпадения из +метабаз как **разные режимы**: догадка LLM показана сверху как «текущий +план», а кандидаты TMDB/TVDB — отдельным списком ниже; «без базы» — +особое состояние. Из-за этого выбор источника непрозрачен: чтобы понять, +куда лягут файлы при другом кандидате, приходится сначала его выбрать +(запись пиннится в overrides) и только потом увидеть результат. Отменить и +попробовать другой — снова вслепую. + +Принцип должен быть иным: **совпадение есть всегда — мы лишь выбираем +источник**. Матч нейронки — такая же строка списка, как кандидаты баз. +Выбор любого источника должен показывать, что получится (поля и целевые +пути), **до применения**. + +## What Changes + +- **Единый список источников совпадения** на странице ревью: строка + «распознано нейронкой» (без базы) наравне с кандидатами метабаз + (TMDB/TVDB/TVMaze) — один список с выбором одного активного источника, + а не «план сверху + кандидаты снизу». +- **Выбор / переключение / отмена в пользу нейронки** как операции над + этим списком: выбрать кандидата базы, переключиться на другого, снять + матч с базой обратно на нейронку — единообразно. +- **Ручное добавление кандидата** по id или URL записи базы, когда + автопоиск промахнулся: добавляется в тот же список как выбираемая строка. +- **Предпросмотр до применения**: при наведении/выборе источника показываем + **поля** (тип, название, год) и **предпросмотр целевых путей раскладки**, + которые получатся при этом источнике, **не пиннит** выбор до явного + подтверждения. Место под «режиссёр» в предпросмотре резервируем (источник + подключим позже — часто есть в контексте загрузки). +- Существующие действия ревью (Применить/Отклонить/Позже/Уточнить/тип/ + игнор/Undo) сохраняются; переработка касается только блока выбора + источника и предпросмотра. + +Вне объёма (осознанно, чтобы не раздувать change): + +- **Детали записи из метабазы** (режиссёр и пр.) через новый вызов + «детали по id» — только резервируем место в UI, сам fetch не делаем. +- **Полноценный редактор маппинга «файл → серия»** — остаётся Ф5. +- Изменения в Telegram — не трогаем (веб = точные правки). + +## Capabilities + +### New Capabilities + +Новых capability не вводим. Отдельный capability `review` (весь процесс +ревью) и перекройка домена метабаз — предмет отдельной задачи беклога +«Пересмотр набора capabilities и рефакторинг спек»; здесь ограничиваемся +существующими capability. + +### Modified Capabilities + +- `web-ui`: рендеринг страницы ревью получает **единый список выбора + источника** (нейронка + кандидаты баз + ручной ввод) и **предпросмотр + полей и целевых путей выбранного источника до применения**. Предпросмотр + берётся из единой логики `internal/layout`, как и текущее превью + раскладки. + +Сбор кандидатов при сверке с базами (`recognition`) переиспользуется как +есть — его поведение не меняется. + +Часть логики этого change доменная (ручное добавление источника, +самосогласованный набор overrides при выборе — см. design.md 1a), а не чисто +презентационная. До выделения отдельного capability `review` (отложено в +беклог) она осознанно живёт под `web-ui`; наблюдаемое поведение выражено +требованиями `web-ui`, а `docs/specs/review-ux.md` остаётся источником истины +по review-домену и обновляется в этом change. + +## Impact + +- **Код:** `internal/httpapi` (обработчик и шаблон страницы ревью, + парсер ручного ввода id/URL, клиентский JS переключения предпросмотра без + пиннинга), `internal/worker` (метод расчёта эфемерного плана + предпросмотра + путей для источника без записи overrides — переиспользует `applyOverrides` + + `layout.BuildLinks`; единая деривация «источник → overrides» с очисткой + title/year в `ChooseCandidate`/`ClearProvider` — заодно чинит залипший + override). Возможна небольшая правка `internal/store` при персистентности + вручную добавленного кандидата. +- **Данные:** новый пользовательский путь добавления кандидата вручную + (provider+id, опц. url); поля title/year у него могут быть пустыми + (деталей из базы пока не тянем). +- **Инварианты:** предпросмотр только строит пути через `layout` (та же + санитизация и проверка границ библиотеки); выбор источника ничего не + раскладывает — хардлинки по-прежнему только по «Применить». +- **Совместимость:** ломающих изменений API/схемы не предполагается; + существующие действия ревью и их семантика сохраняются. diff --git a/openspec/changes/review-source-selection/specs/web-ui/spec.md b/openspec/changes/review-source-selection/specs/web-ui/spec.md new file mode 100644 index 0000000..859d122 --- /dev/null +++ b/openspec/changes/review-source-selection/specs/web-ui/spec.md @@ -0,0 +1,144 @@ +## ADDED Requirements + +### 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 создавать хардлинки: +сохранённый матч меняется только явным выбором источника, а раскладка — +только действием «Применить». Совпадение целевых путей предпросмотра с +результатом применения регулируется требованием «Превью раскладки через +единую логику именования». + +#### Scenario: Предпросмотр полей без фиксации выбора + +- **GIVEN** список источников на экране ревью +- **WHEN** пользователь рассматривает источник, ещё не выбрав его активным +- **THEN** показаны поля результата (тип, название, год) для этого источника +- **AND** сохранённый матч загрузки не меняется, хардлинки не создаются + +#### Scenario: Зарезервированное место под режиссёра + +- **GIVEN** режиссёр из метабазы пока не загружается +- **WHEN** отображается предпросмотр полей источника +- **THEN** в предпросмотре присутствует место под режиссёра, показанное + пустым (или прочерком), не ломая вёрстку + +## MODIFIED Requirements + +### Requirement: Превью раскладки через единую логику именования + +Превью целевых путей раскладки в веб-UI SHALL вычисляться той же логикой +именования, что и реальная раскладка (`internal/naming`/`internal/layout`), а +не дублировать правила в шаблоне. На экране ревью превью SHALL строиться **для +каждого источника в списке** (нейронка, кандидат базы, добавленный вручную) — +эфемерно на сервере, без записи сохранённого матча. Показанные для источника +пути MUST совпадать с теми, что создались бы при выборе этого источника и +применении. + +#### Scenario: Превью совпадает с реальной раскладкой + +- **WHEN** на экране ревью отображается превью целевых путей для источника +- **THEN** эти пути идентичны тем, что создаст применение при выборе этого + источника (те же правила имён, спецвыпусков, мультифайла, запрещённых + символов, тега провайдера и коллизий) + +#### Scenario: Переключение источника не тянет чужие поля + +- **GIVEN** активен кандидат с запиненными название/год, затем выбран + источник без собственных названия/года (нейронка или ручной кандидат) +- **WHEN** строится превью и затем выполняется применение выбранного источника +- **THEN** и превью, и применение используют название/год этого источника + (из плана распознавания), без унаследованных от прежнего кандидата + +### Requirement: Матч с записью метабазы ссылкой + +Веб-UI SHALL показывать подтверждённый матч с записью метабазы (TMDB/TVDB/IMDb) +как ссылку на эту запись — на странице просмотра `/download/{id}` (блок +распознавания) и в едином списке источников совпадения экрана ревью (у +активного источника-кандидата). Ссылка SHALL открываться в новой вкладке с +`rel="noopener"`. Рядом со ссылкой SHALL быть видны провайдер, идентификатор +записи и (при наличии) год. + +#### Scenario: Матч виден ссылкой на странице просмотра + +- **WHEN** у загрузки подтверждён матч с записью метабазы и известен URL записи +- **THEN** в блоке распознавания на `/download/{id}` матч показан ссылкой на + запись с провайдером и id + +#### Scenario: URL записи неизвестен + +- **WHEN** матч подтверждён (например, id задан вручную), но канонический URL + записи построить нельзя +- **THEN** матч показывается текстом (провайдер и id) без ссылки, строка списка + не ломается diff --git a/openspec/changes/review-source-selection/tasks.md b/openspec/changes/review-source-selection/tasks.md new file mode 100644 index 0000000..3eb3bda --- /dev/null +++ b/openspec/changes/review-source-selection/tasks.md @@ -0,0 +1,61 @@ +## 1. Ядро: эфемерный предпросмотр источника (worker) + +- [ ] 1.1 Ввести чистую деривацию «источник → полный самосогласованный набор + overrides» (кандидат с title/year → пиннит; без — пустая строка в + title/year; нейронка → provider=none + пустые title/year) и + переиспользовать её в превью и в коммите — гарантия preview == apply + (решение 1a design.md) +- [ ] 1.2 Привести команды выбора к этой деривации: `ChooseCandidate` у + безтайтлового кандидата и `ClearProvider` очищают `ovrTitle`/`ovrYear` + (пустой строкой) — чинит латентный залипший override +- [ ] 1.3 Выделить чистый расчёт «план + набор overrides источника → поля + + `[]layout.Link`» из логики `ReviewData` (переиспользуя `applyOverrides` + + `toLayoutPlan` + `layouter.BuildLinks`), без записи overrides в БД +- [ ] 1.4 Расширить `ReviewData` срезом источников: нейронка (синтетическая + строка `provider=none`) + каждый кандидат; для каждого — эффективные + поля (тип/название/год), превью путей и признак «активен»; дедуп + источников по `provider:id` (нейронка — отдельная строка) +- [ ] 1.5 Тесты worker: превью для неактивного источника не пишет overrides + и не создаёт ссылок; пути превью совпадают с результатом применения + того же источника; **переключение с титульного кандидата на нейронку/ + безтайтловый источник не тянет чужие название/год** (preview и apply); + нейронка-строка = раскладка без тега провайдера + +## 2. Ручное добавление источника (worker + store + httpapi) + +- [ ] 2.1 Парсер ручного ввода в `httpapi`: id или URL записи метабазы → + `(provider, provider_id)` (обратный к `providerURL`: themoviedb.org + movie/tv, thetvdb, imdb); валидация на входной границе +- [ ] 2.2 Метод worker «добавить источник вручную»: сохранить + `metadata_candidate` (provider/id, опц. url; title/year пустые) с + дедупом по `provider:id`, затем выбрать его (как `ChooseCandidate`) +- [ ] 2.3 Обработчик POST добавления ручного источника + маршрут; отклонять + некорректный ввод сообщением, не меняя активный источник +- [ ] 2.4 Тесты: URL → provider+id; невалидный ввод отклонён; дубль id не + плодит строку, а выбирает существующую + +## 3. Страница ревью: единый список и предпросмотр (httpapi + шаблон) + +- [ ] 3.1 Переработать блок «Источник совпадения» в единый список строк: + нейронка + кандидаты баз + ручной ввод; ровно один активный +- [ ] 3.2 Рендер строк с предпросмотром полей (тип/название/год + пустое + место под режиссёра) и целевых путей для каждого источника +- [ ] 3.3 Клиентский vanilla-JS: переключение видимости предпосчитанного + превью по выбору строки; без доменного пересчёта на клиенте +- [ ] 3.4 Действия строк формами-раундтрипами: выбрать кандидата + (`ChooseCandidate`), снять в пользу нейронки (`ClearProvider`), + добавить вручную; сохранить прежние действия ревью +- [ ] 3.5 Убедиться, что матч-ссылка и заголовок корректны для ручного + кандидата с пустыми title/year + +## 4. Проверка и приёмка + +- [ ] 4.1 `task test` и `task lint` зелёные +- [ ] 4.2 Ручная проверка сценариев спеки: нейронка-строка, переключение + кандидатов, снятие в пользу нейронки, ручной ввод по URL, предпросмотр + без фиксации +- [ ] 4.3 `openspec validate review-source-selection --strict` +- [ ] 4.4 Обновить `docs/specs/review-ux.md` (источник истины по review-домену + до миграции): единый список источников, ручное добавление, предпросмотр, + объём Ф3/Ф5 — обязательно (поведение экрана меняется); убрать пункт + беклога после archive