Files
jellybit/openspec/specs/metadata-match/spec.md
T
avandClaude Opus 4.8 512567c8ba Рефакторинг границ 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>
2026-07-03 21:17:51 +03:00

131 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 доступен без повторной
генерации