## 1. Разведка формы ответа

- [x] 1.1 Записать `docs/research/tvdb-search-translations.md`: форма `SearchResult`
      (`name`, `translations`, `name_translated`, `primary_language`), смысл
      параметра `language` у `/search`, версия swagger, честный провенанс
      «сверено по документации, живым прогоном не подтверждено»
- [x] 1.2 Добавить запись в перечень `docs/research/README.md` → «Записи»

## 2. Клиент TVDB

- [x] 2.1 `TVDBConfig.Language` — абстрактный код `ru`|`en`, комментарий по образцу
      `TMDBConfig.Language`
- [x] 2.2 `tvdbLocale(lang string) string` — тотальный `switch`, `ru` → `rus`,
      default → `eng`; имя ровно как у соседа `tmdbLocale`; комментарий называет
      `config.validate` источником множества
- [x] 2.3 `NewTVDB` кладёт выведенный код в поле клиента
- [x] 2.4 `tvdbSearchResp` объявляет `translations` как `json.RawMessage` и
      раскладывает в `map[string]string` отдельно, с гашением ошибки: негодная
      форма косметического поля не должна ронять разбор всей выдачи
- [x] 2.5 `Search` заполняет `Title` из перевода (ключ ищется `strings.EqualFold`,
      выбор среди совпавших детерминирован, значение — после `strings.TrimSpace`)
      с фолбэком на `name`, и `OriginalTitle` из `name`
- [x] 2.6 Убедиться, что параметр `language` в строку запроса `/search` НЕ попал
- [x] 2.7 Сигнал на операцию, когда во всей выдаче не встретилось ни одного ключа
      языка ожидаемого вида: подозрение на иную форму ответа отличается от
      штатного «перевода нет». Уровень `WARN` — `DEBUG` в проде выключен
      (правка по находке 1 ревью кода)
- [x] 2.8 Дописать третий маппер (`metadata.tvdbLocale`) в перечень потребителей
      кода языка в комментарии `internal/config/config.go` → `validate`

## 3. Проброс из точки входа

- [x] 3.1 `cmd/jellybit/serve.go` — `metadataProviders` передаёт
      `Language: cfg.ContentLanguage()` в `TVDBConfig`

## 4. Тесты

- [x] 4.1 Стенд `fakeTVDB` в `tvdb_test.go` отдаёт запись с блоком `translations`
- [x] 4.2 Табличный тест: перевод есть → `Title` локализован; перевода на нужный
      язык нет; перевод из одних пробелов; ключ в другом регистре; блока нет;
      блок пришёл не картой. Во всех случаях, кроме первого, `Title` = primary
      name; во всех без исключения `OriginalTitle` = primary name
- [x] 4.3 Тест на язык по умолчанию (`Language` не задан → берётся `eng`)
- [x] 4.4 Тест: строка запроса `/search` не содержит `language` ни при `ru`, ни при
      пустом языке, и совпадает в обоих случаях
- [x] 4.5 Тест: негодный блок переводов не проваливает `Search` — кандидаты
      возвращаются
- [x] 4.6 Тест: на один `Search` уходит ровно один HTTP-запрос к `/search`
      (счётчик на стенде) — расход лимита ключа не растёт
- [x] 4.8 Тест на сигнал о неожиданной форме ответа: буфер логгера, 8 случаев,
      включая штатный «перевода нет» с ожиданием отсутствия следа
- [x] 4.9 Тест на детерминизм выбора ключа перевода (200 прогонов на случай)
- [x] 4.10 Тест `TestMatchMetadata_TranslationMakesTwoStrong` в
      `internal/recognize/metadata_test.go`: два кандидата совпадают с планом
      разными осями — подтверждённого матча нет, обе записи уходят в review
- [x] 4.7 Интеграционный тест в `integration_test.go` за `TVDB_API_KEY` печатает
      `Title` и `OriginalTitle`. **Пишется, но не запускается** — живых обращений
      к TVDB в этой задаче нет

## 5. Учёт нерешённого

- [x] 5.1 Записать вопрос человеку в `docs/tasks/items/tvdb-title-locale.md`
      (раздел «Вопросы» + тег `question`) через скилл `av-dev-pm:tasks`: форма
      ответа TVDB не сверена живым API, нужен ручной прогон под ключом; плюс
      расхождение с критерием приёмки A1 про параметр языка в запросе; плюс
      двустороннее движение границы авто/review. **Сделано иначе, чем
      написано**: раздел «Вопросы» у задачи в спринте краснит `tasks.py check`
      и через него шаг `canon` гейта, а закрытие задачи стёрло бы вопрос вместе
      с файлом. Вопрос вынесен отдельной задачей-разведкой
      `docs/tasks/items/tvdb-search-response-live-check.md`

## 6. Гейт

- [x] 6.1 `openspec validate --strict tvdb-title-locale`
- [x] 6.2 `task gate` зелёный

## Критерии приёмки задачи

Пришли из `docs/tasks/items/tvdb-title-locale.md`; переписывать и занижать их
нельзя, исход по каждому идёт в доклад.

- [ ] A1 Запрос поиска TVDB содержит параметр языка, выведенный из
      `[general].language` (оракул: тест на `httptest`-сервере в `tvdb_test.go`).
      **Отменён решением 1 design.md**: параметр — фильтр выдачи, а не селектор
      перевода. Вместо него проверяется обратное — параметра в запросе нет
      (задача 4.4). Расхождение вынесено вопросом человеку
- [x] A2 При наличии перевода `Candidate.Title` приходит на языке настройки, при
      отсутствии — равен primary name (оракул: табличный тест 4.2)
- [x] A3 `Candidate.OriginalTitle` у TVDB непуст и равен primary name (оракул: тот
      же тест 4.2; интеграционный прогон за `TVDB_API_KEY` печатает оба поля —
      **написан, но не запущен**, живых обращений к метабазе в задаче нет)
- [x] A4 Дельта-спека `metadata-match` заказывает локаль TVDB и фолбэк на оригинал
      (оракул: `openspec validate --strict`)

## Приёмочные критерии от рубрики (ревью дизайна, проход `rubric`)

Рубрика порождена до чтения кода и дизайна. Пункты 1, 5, 7 закрываются **условно**:
они опираются на форму ответа `/search`, взятую из публичной документации, — стенд
`httptest` проверит поведение кода на предполагаемой форме, а не истинность
предположения.

- [x] R1 Фолбэк `Title` тотален по всем формам отсутствия перевода: блока нет; блок
      пуст; ключа нет; значение пустое. Пустого `Title` при непустом `name` нет ни
      на одной ветви. Оракул — строка табличного теста на каждую форму *(условно)*
- [x] R2 Строка запроса `/search` не содержит параметра языка ни при каком значении
      настройки: утверждение об **отсутствии** ключа плюс равенство URL при `ru` и `en`
- [x] R3 `OriginalTitle` = primary name всегда, включая случай найденного перевода;
      значение настройки языка его не смещает
- [x] R4 Язык имеет один источник и один вывод диалекта: ни константы
      `tvdbDefaultLanguage`, ни фолбэка в конструкторе, ни второго места перевода
      `ru`/`en` → `rus`/`eng`
- [x] R5 Разбор устойчив к форме, а не только к содержимому: лишние поля,
      отсутствующие поля, `translations: null`, `translations` не картой, усечённое
      тело — не паникуют и не проваливают `Search`; худший исход — фолбэк по R1.
      Негодный ответ поиска **целиком** по-прежнему остаётся ошибкой *(условно)*
- [x] R6 Пустота значения проверяется **после** `TrimSpace`, а не до
- [x] R7 Ключ в неожиданной форме (иной регистр) не даёт молчаливого фолбэка: поиск
      ключа регистронезависимый, исход зафиксирован тестом *(условно)*
- [x] R8 Число обращений к TVDB на одну сверку не растёт: один `Search` — один
      HTTP-запрос, проверяется счётчиком на стенде
- [x] R9 Результат — функция ответа провайдера, а не порядка проходов: исход не
      зависит от того, каким проходом пришёл кандидат; заполнение `OriginalTitle`
      не заставляет следующий заход перебора искать по подменённому значению; на
      фолбэке `Title == OriginalTitle` дедупликация ключей перебора не схлопывает
      перебор до нуля запросов
- [x] R10 Секрет и контекст ведут себя как у остальных клиентов: ключ TVDB не
      попадает в лог, в текст ошибки разбора и в сообщение с URL; `context`
      доходит до запроса и отменяет его; таймаут из конфига
- [x] R11 У ответа есть предел (`maxBody` в `rawGet`), у выдачи — прежний потолок
      кандидатов; второго канала без предела не заводится
- [x] R12 Факт фолбэка наблюдаем, но не шумен: один чекпоинт на операцию,
      отсутствие перевода — не `ERROR`, недоступность опциональной метабазы
      состояние загрузки не двигает
