## 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`, недоступность опциональной метабазы состояние загрузки не двигает