Рефакторинг границ capabilities: цепочка загрузка→матч→ревью→раскладка (openspec)
Привёл набор capabilities в OpenSpec к цепочке обработки, чтобы имя capability отвечало одному поведению. Чисто по спекам, код и поведение системы не меняются. Change refactor-capability-boundaries (архивирован): - recognition разделён на recognition (разбор LLM) + metadata-match (сверка с базами) - review выделен из web-ui + мигрирован из docs/specs/review-ux.md - новые capability из docs/specs: file-layout, download-tracking, notifications - identity очищен до инфра-id; приём (инфохэши, дедуп, ядро приёма) — в ingest - уведомление о рассинхроне перенесено из state-reconciliation в notifications - дубль владения путём и безопасного undo оставлен в state-reconciliation Итог: 11 capabilities, openspec validate --strict проходит (+37/−11 требований). Источник истины по мигрированным темам переехал в openspec/specs (шапки в docs). Снят пункт беклога «Пересмотр набора capabilities». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# metadata-match Specification
|
||||
|
||||
## Purpose
|
||||
Сверка распознанного плана с внешними базами метаданных (TMDB/TVDB/TVMaze):
|
||||
поиск записи по нескольким названиям с нормализацией, локаль запроса,
|
||||
подтверждение единичного сильного матча (официальный `provider_id` +
|
||||
каноническое имя/год) и сбор кандидатов с URL для ручного выбора в `review`.
|
||||
Разбор сигналов моделью — в `recognition`.
|
||||
## Requirements
|
||||
### Requirement: Сверка с базой по нескольким названиям
|
||||
|
||||
При сверке плана с включёнными базами метаданных система SHALL искать по
|
||||
нескольким названиям в порядке убывания силы ключа: сначала по
|
||||
`original_title`, затем по локализованному `title`, затем по `provider_hint`.
|
||||
Поиск SHALL останавливаться, как только очередной запрос дал единичный
|
||||
сильный матч (ровно один кандидат с совпадением названия и года). Запрос с
|
||||
названием, нормализованно совпадающим с уже выполненным, система SHALL
|
||||
пропускать, чтобы не обращаться к базе повторно с тем же ключом.
|
||||
|
||||
Кандидаты для ручного выбора в review система SHALL собирать из всех
|
||||
выполненных заходов с дедупликацией по `provider:id` и общим потолком.
|
||||
|
||||
#### Scenario: Иностранный фильм находится по оригинальному названию
|
||||
|
||||
- **GIVEN** план с `title` «Тёмный рыцарь», `original_title` «The Dark Knight», год 2008
|
||||
- **WHEN** выполняется сверка с базой
|
||||
- **THEN** первый запрос идёт по «The Dark Knight»
|
||||
- **AND** при единичном сильном матче дальнейшие запросы (по `title`, `provider_hint`) не выполняются
|
||||
|
||||
#### Scenario: Фолбэк на локализованное название
|
||||
|
||||
- **GIVEN** план, для которого запрос по `original_title` не дал единичного сильного матча
|
||||
- **WHEN** продолжается сверка
|
||||
- **THEN** выполняется запрос по локализованному `title`
|
||||
- **AND** при отсутствии матча и там — запрос по `provider_hint`
|
||||
|
||||
#### Scenario: Дублирующий запрос пропускается
|
||||
|
||||
- **GIVEN** план, у которого `original_title` нормализованно совпадает с `title`
|
||||
- **WHEN** выполняется сверка
|
||||
- **THEN** база запрашивается этим названием один раз, повторный заход по `title` не делается
|
||||
|
||||
### Requirement: Подтверждение матча и каноническое имя
|
||||
|
||||
При единичном сильном матче система SHALL брать из записи базы официальный
|
||||
`provider` (`tmdb`|`tvdb`|`tvmaze`) и `provider_id`, а также каноническое название
|
||||
и год, и подменять ими соответствующие поля плана (для сериала — с учётом внешнего
|
||||
тега TVDB/IMDb из `externals`, идущего в имя папки). Матч SHALL считаться
|
||||
подтверждённым только при ровно одном сильном кандидате; при нуле или нескольких
|
||||
кандидатах подтверждённого матча быть SHALL NOT (авто-раскладка не разрешается,
|
||||
кандидаты уходят в review). Работа с базами опциональна: при выключенных базах
|
||||
сверка не выполняется и подтверждённого матча нет.
|
||||
|
||||
#### Scenario: Единичный матч даёт id и каноническое имя
|
||||
|
||||
- **GIVEN** поиск вернул ровно одного сильного кандидата TMDB для фильма
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** план получает `provider`=`tmdb`, `provider_id`, каноническое название и год
|
||||
|
||||
#### Scenario: Несколько кандидатов — матч не подтверждён
|
||||
|
||||
- **GIVEN** поиск вернул более одного подходящего кандидата
|
||||
- **WHEN** оценивается матч
|
||||
- **THEN** подтверждённого матча нет, кандидаты собираются для выбора в review
|
||||
|
||||
### Requirement: Локаль запроса к TMDB
|
||||
|
||||
Запрос поиска к TMDB SHALL передавать параметр `language`, по умолчанию
|
||||
`ru-RU`, со значением, настраиваемым конфигом `[metadata.tmdb].language`.
|
||||
Это влияет только на локализованное поле `Title`/`Name`; поле
|
||||
`original_title`/`original_name` остаётся на языке оригинала, поэтому
|
||||
оригинальная сторона сравнения не затрагивается.
|
||||
|
||||
#### Scenario: Локализованный заголовок приходит по-русски
|
||||
|
||||
- **GIVEN** TMDB включён, `language` не задан в конфиге
|
||||
- **WHEN** выполняется поиск фильма с русской локализацией
|
||||
- **THEN** запрос содержит `language=ru-RU`
|
||||
- **AND** в кандидате `Title` приходит на русском, а `OriginalTitle` — на языке оригинала
|
||||
|
||||
### Requirement: Нормализация названий при сравнении
|
||||
|
||||
Нормализация названий для гейта сильного матча SHALL сводить букву `ё` к `е`,
|
||||
чтобы написания, различающиеся только `ё`/`е`, считались одним названием.
|
||||
|
||||
#### Scenario: «Тёмный» и «Темный» совпадают
|
||||
|
||||
- **GIVEN** план с названием «Тёмный рыцарь» и кандидат базы «Темный рыцарь»
|
||||
- **WHEN** сравниваются нормализованные названия
|
||||
- **THEN** они считаются совпадающими
|
||||
|
||||
### Requirement: Кандидат несёт URL для внешней проверки
|
||||
|
||||
Каждый кандидат внешней базы метаданных (`metadata.Candidate`) SHALL нести
|
||||
поле `URL` — ссылку на страницу элемента (фильма/сериала) на сайте
|
||||
провайдера. URL SHALL формироваться клиентом провайдера при поиске
|
||||
(`Search`) и сохраняться в таблице `metadata_candidate`. Отображение этой
|
||||
ссылки на экране ревью — забота `review`/`web-ui`, не данного требования.
|
||||
|
||||
Формат URL для каждого провайдера:
|
||||
|
||||
- **TMDB**: `https://www.themoviedb.org/movie/{id}` (фильм) или
|
||||
`https://www.themoviedb.org/tv/{id}` (сериал) — тип контента известен из
|
||||
запроса `Query.Type`
|
||||
- **TVDB**: `https://www.thetvdb.com/dereferrer/series/{id}`
|
||||
- **TVMaze**: `https://www.tvmaze.com/shows/{id}` — URL SHALL использовать
|
||||
нативный id TVMaze, а не внешний тег (TVDB/IMDb), чтобы ссылка вела на
|
||||
TVMaze-страницу
|
||||
|
||||
#### Scenario: Кандидат TMDB с корректной ссылкой
|
||||
|
||||
- **GIVEN** TMDB найден кандидат-фильм с id `603` («Матрица»)
|
||||
- **WHEN** клиент TMDB формирует Candidate
|
||||
- **THEN** `URL` = `https://www.themoviedb.org/movie/603`
|
||||
|
||||
#### Scenario: Кандидат TVMaze с нативной ссылкой
|
||||
|
||||
- **GIVEN** TVMaze найден сериал с id `169` («Фарго»), внешний тег — TVDB id `269613`
|
||||
- **WHEN** клиент TVMaze формирует Candidate
|
||||
- **THEN** `URL` = `https://www.tvmaze.com/shows/169`
|
||||
- **AND** `TagProvider`/`TagID` остаются `tvdb`/`269613` (тег папки Jellyfin не меняется)
|
||||
|
||||
#### Scenario: URL сохраняется в БД
|
||||
|
||||
- **GIVEN** результат поиска с кандидатами
|
||||
- **WHEN** кандидаты сохраняются в таблицу `metadata_candidate`
|
||||
- **THEN** значение `url` SHALL быть записано в колонку `url`
|
||||
- **AND** при последующей загрузке данных ревью url доступен без повторной
|
||||
генерации
|
||||
|
||||
Reference in New Issue
Block a user