metadata: TVDB отдаёт локализованное название и оригинал
- локаль из [general].language применяется при разборе ответа /search, а в запрос не уходит: параметр language у TVDB — фильтр выдачи, а не селектор перевода (ADR-2026-08-07) - Title берётся из блока translations с тотальным фолбэком на primary name, OriginalTitle — из primary name; форма ответа сверена по документации и живым прогоном не подтверждена (docs/research) - неожиданная форма ответа даёт WARN: признак — отсутствие во всей выдаче ключей языка ожидаемого вида, а не неудача разбора блока
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# Ревью изменения `tvdb-title-locale` — отчёт триажа
|
||||
|
||||
Сформирован проходом `review-triage`; записан оркестратором (у прохода запись файлов
|
||||
заблокирована). В конце добавлена секция «Что сделано по находкам».
|
||||
|
||||
## Сводка
|
||||
|
||||
**Размер:** малое (один содержательный узел `internal/metadata/tvdb.go` + тривиальная проводка в
|
||||
`cmd/jellybit/serve.go`, комментарий в `internal/config/config.go`, два документных артефакта).
|
||||
**Сложность:** незнакомое (дословно сработал проектный триггер `docs/review.md` → «Незнакомое
|
||||
здесь»: новый источник в слиянии кандидата метабазы — `OriginalTitle` у TVDB становится непустым
|
||||
и входит новой осью сравнения в `strongMatches`; внешний контракт живым прогоном не подтверждён).
|
||||
**Метка:** `large` (малое × незнакомое), не спорный случай.
|
||||
**Режим прогона:** по графу. **Гейт:** зелёный (`BASE=master task gate`, `-race` реально гонялся —
|
||||
gcc есть; `gitleaks` no leaks; `govulncheck` 0 достижимых).
|
||||
|
||||
**Сигнал о заниженной метке:** пришёл от `code` — «сигнала нет, метка `large` адекватна».
|
||||
`basics` не запускался по плану, второго провенанса нет.
|
||||
|
||||
### План разметки с исходом по каждой теме
|
||||
|
||||
| тема | дом | глубина | кто закрывает | исход |
|
||||
|---|---|---|---|---|
|
||||
| requirements | `openspec/specs/metadata-match/spec.md` + дельта | разбор | `specs` | закрыта, 4 находки (S1–S4) |
|
||||
| autotests | `CLAUDE.md` → «Гейт» | — | `autotests` | закрыта, 1 находка (A1), отработана до опиниативных проходов |
|
||||
| conventions | `docs/conventions/README.md` + `config.md` + `logging.md` | разбор | `code` | закрыта, 3 находки (C1–C3) |
|
||||
| architecture | `docs/architecture.md` → «Единые точки» (+ `docs/passport.md`) | доказательство | `architecture` | закрыта, 3 находки (R1–R3) + 4 пункта «дешевле переделать до мерджа» |
|
||||
| security | `docs/security.md` | доказательство | `adversary` | закрыта, 4 находки (AD1–AD4); три вопроса темы — вне диффа, не разбирались |
|
||||
| operations | `docs/architecture.md` → «Эксплуатация» (+ `docs/database.md`) | доказательство | `ops` | закрыта, 1 находка (O1) + 5 снятых замерами гипотез |
|
||||
| тема проекта | своих тем проекта нет | — | `basics` не запускается | дома у темы нет; отсутствие отчёта согласовано планом |
|
||||
|
||||
**Тем без отчёта нет.**
|
||||
|
||||
Находок на входе — 17. Различных причин после дедупликации — 10. Крупнейшее слияние: «след не
|
||||
срабатывает» — пять независимых обнаружений (S1, C1, R1, AD2, O1) плюс S2 как отсутствующий на
|
||||
него тест. Это не подтверждение: под всеми проходами одна модель с одними априорными; согласие
|
||||
подняло приоритет, `Confidence` — нет. Оракул добыт триажом отдельно. В основном списке 6
|
||||
(2 блокирующих + 4 «сейчас»), потолок 3+4 не выбран.
|
||||
|
||||
**A1 (закрыта до опиниативных проходов):** гейт краснел по шагу `canon` — задача в спринте с
|
||||
непустым разделом «Вопросы». Вопрос вынесен задачей-разведкой
|
||||
`docs/tasks/items/tvdb-search-response-live-check.md`, раздел убран, гейт перегнан.
|
||||
|
||||
## Блокирует мердж
|
||||
|
||||
### 1. Единственный компенсирующий контроль изменения нем ровно на том риске, ради которого заведён
|
||||
|
||||
- Файл: `internal/metadata/tvdb.go:191-205,247-252`; дельта, сценарий «Блок переводов отсутствует
|
||||
или пришёл негодной формой»
|
||||
- Severity: major · Confidence: high
|
||||
- Оракул: падающий тест на настоящем `TVDB.Search` (httptest-стенд, буфер логгера):
|
||||
`translations={"ru":…,"en":…}` → след false FAIL; `null` → false FAIL; `{}` → false FAIL;
|
||||
`{"rus":{"name":…}}` → true PASS; `["Дюна"]` → true PASS; поля нет → true PASS.
|
||||
Плюс `docs/conventions/logging.md` — «`DEBUG` … в проде выключен», и дефолт `[log].level = "info"`.
|
||||
- Последствие: два независимых компаундирующих отказа одного механизма. **Предикат:**
|
||||
`json.Unmarshal("null", &map[string]string{})` возвращает `err == nil` и `nil`-карту, поэтому для
|
||||
`null`, `{}` и **двухбуквенных кодов** признак «разобралось» истинен — а двухбуквенные коды
|
||||
`design.md` называет главным риском поимённо. **Уровень:** даже сработавший след при штатном
|
||||
деплое не виден. Итог: все названия тихо уедут в фолбэк на английский primary name, и
|
||||
единственный заведённый способ это заметить не сработает.
|
||||
- Найдено проходами: `specs` (S1), `code` (C1), `architecture` (R1), `adversary` (AD2), `ops` (O1)
|
||||
- Действие: **развилка**. (а) починить предикат и поднять уровень до `WARN`, закрепить тестом, в
|
||||
спеке заказывать наблюдаемость, а не уровень; (б) признать детектор нерабочим и снять;
|
||||
(в) оставить код, переписать дельту под фактическое поведение. Рекомендация — (а).
|
||||
|
||||
### 2. Название из метабазы уезжает в имя каталога Jellyfin дословно, мимо санитайзинга, авто-раскладкой
|
||||
|
||||
- Файлы: `internal/metadata/tvdb.go:233-245`; `internal/recognize/recognize.go:261-270`;
|
||||
`design.md` → Risks
|
||||
- Severity: major · Confidence: high
|
||||
- Оракул: падающий тест на реальном `Recognizer`: `plan.Title` из трёх ZWSP — `auto=true`, тогда как
|
||||
`sanitizeTitle` дал бы пустую строку; `"Dune<RLO>gnp.mkv"`, `"Dune\nHACK"`, `"Dunа"` с кириллической
|
||||
`а` — все с `auto=true` и все отличаются от того, что дал бы санитайзер. Плюс чтение кода:
|
||||
`plan.Title = match.Title` стоит **после** `sanitizePlan`, а гейт сверяет
|
||||
`normalize(c.Title) || normalize(c.OriginalTitle)` — проходит по `OriginalTitle`, и значение,
|
||||
уезжающее в план, не сверяется ни с чем.
|
||||
- Последствие: `layout.sanitizeComponent` снимает `/\:*?"<>|` и `< 0x20`, но не трогает категорию
|
||||
Cf и не сворачивает гомоглифы. `adversary` показал два визуально неотличимых каталога.
|
||||
Инвариант «целевой путь строго под библиотекой» **держится** (проверено на `Dune/../../etc`,
|
||||
`" .. "`, `"..."`, 400 символов) — поэтому не `critical`. Класс **пре-существующий**: у TMDB тот
|
||||
же путь. Дифф распространяет его на второго провайдера и впервые для TVDB делает поле,
|
||||
уезжающее в путь, несверяемым. Отдельно: именно ложной фразой в `design.md` обоснован вывод
|
||||
«новой недоверенной границы не появляется», и она архивируется вместе с change.
|
||||
- Найдено проходами: `adversary` (AD1), независимо — `architecture`
|
||||
- Действие: **развилка**. Прямой ответ на вопрос задания: **починка кода — урожай, а не дефект
|
||||
этой задачи; починка `design.md` — дефект этой задачи и делается инлайн.** Вариант с
|
||||
`sanitizeTitle` к `match.Title` меняет поведение уже работающего TMDB и выходит за рамку задачи.
|
||||
Рекомендация — поправить документ, завести задачу.
|
||||
|
||||
## Стоит исправить сейчас
|
||||
|
||||
### 3. Один и тот же байт-в-байт ответ TVDB даёт разное имя каталога от прогона к прогону
|
||||
- Файл: `internal/metadata/tvdb.go:199-203` · minor · medium (механизм доказан, достижимость нет)
|
||||
- Оракул: 500 разборов одного тела — `{"rus":"Дюна","RUS":"HACK"}` дал `map[HACK:59 Дюна:441]`;
|
||||
`{"rus":"Дюна","ruſ":"HACK"}` → `map[HACK:63 Дюна:437]`.
|
||||
- Последствие: `for k := range m` по карте (порядок рандомизирован) + `EqualFold`, совпадающий с
|
||||
несколькими ключами. Две задачи по одной раздаче в разное время получат разные имена каталогов.
|
||||
- Сопутствующая правка спеки (провенанс `specs`/S3): регистронезависимый поиск дельтой не заказан
|
||||
вовсе — живёт только в `design.md` и строке теста, а `design.md` архивируется.
|
||||
- Действие: **инлайн**
|
||||
|
||||
### 4. Сдвиг границы авто-review подтверждён только рассуждением
|
||||
- Файл: дельта, сценарий про двух сильных кандидатов; механизм — `internal/recognize/metadata.go`
|
||||
· minor · high
|
||||
- Оракул: в `internal/recognize/metadata_test.go` нет теста, где два кандидата в пределах года и
|
||||
один совпадает по `Title`, а другой по `OriginalTitle`.
|
||||
- Последствие: на счётчике сильных кандидатов держится инвариант «авто-раскладка только при
|
||||
подтверждённом матче» (`major`). Регрессия в `strongMatches` гейт не покраснит.
|
||||
- Действие: **инлайн**
|
||||
|
||||
### 5. Поле `language` в логах несёт два разных словаря
|
||||
- Файлы: `internal/metadata/tvdb.go:251` в паре с `cmd/jellybit/serve.go:109` · nit · high
|
||||
- Оракул: `docs/conventions/logging.md` → «Поля: словарь имён» — «одно поле — одно имя по всему
|
||||
коду». `serve.go` кладёт абстрактный код (`ru`), новая строка — диалект (`rus`).
|
||||
- Действие: **инлайн**
|
||||
|
||||
### 6. Образец конфига после этого изменения говорит про локаль TVDB неправду
|
||||
- Файл: `config.example.toml`, секция `[metadata.tvdb]` · nit · high
|
||||
- Оракул: `docs/conventions/config.md` → самодокументируемый образец; у `[metadata.tmdb]` хвост
|
||||
про `[general].language` есть, у `[metadata.tvdb]` нет — до change асимметрия была верна.
|
||||
- Действие: **инлайн**
|
||||
|
||||
## Гипотезы без доказательства
|
||||
|
||||
1. Достижимость находки 3 (два EqualFold-равных ключа от живого TVDB) не проверена и в рамке
|
||||
прогона проверена быть не может.
|
||||
2. **Основание всего изменения — форма блока `translations` — живым прогоном не подтверждено.**
|
||||
Все находки про перевод условны на этом. Единственный оракул — `TestIntegration_TVDB` за
|
||||
`TVDB_API_KEY`, прогоняет человек. Отдельно от `specs`: **интеграционный тест печатает
|
||||
результат по `Ne Zha`, но ничего не утверждает** — `PASS` с пустым выводом неотличим от
|
||||
подтверждения.
|
||||
3. AD4 (название длиннее ~237 байт уводит задачу в `failed` вместо review) понижено не по
|
||||
доказательности (оракул есть: `n=250` → `Apply err=… file name too long`, `results=0`), а по
|
||||
принадлежности к диффу: путь не тронут изменением.
|
||||
4. Согласие пяти проходов по находке 1 — не подтверждение. `Confidence: high` стоит из-за
|
||||
падающего теста, добытого триажом, и ни по какой другой причине.
|
||||
|
||||
## Promote candidates
|
||||
|
||||
1. **Правило `internal/archrules`, связывающее множество кодов языка с мапперами** (провенанс
|
||||
`architecture`/R2). Язык вывода живёт в пяти местах четырёх пакетов; тестов, связывающих
|
||||
множество с мапперами, нет — забытая ветка молча даст английский вывод. Дешёвый паллиатив:
|
||||
строка «язык вывода» в `docs/architecture.md` → «Единые точки проекта».
|
||||
2. **Один стенд чужого API на пакет** (провенанс `architecture`/R3). В `tvdb_test.go` два фейка
|
||||
одного API; когда форма ответа поменяется по факту разведки, забытый `fakeTVDB` останется
|
||||
зелёным и продолжит подтверждать опровергнутое предположение.
|
||||
3. **Интеграционный тест обязан утверждать, а не печатать** (провенанс `specs`).
|
||||
|
||||
**Отсеяно как вкусовщина:** переименование `translatedName` (поведения не меняет, записанной
|
||||
конвенции нет); асимметрия «primary name едет без `TrimSpace`» (последствия нет); `searches` как
|
||||
дубль `len(queries)` — часть promote-кандидата 2. Раздел «Типовые ложноположительные»
|
||||
`docs/review.md` прочитан; ни одна находка под них не подпала.
|
||||
|
||||
## Границы покрытия
|
||||
|
||||
**План:** все семь строк имеют исход; тем без дома одна и она объявлена планом заранее; тем без
|
||||
отчёта — ноль.
|
||||
|
||||
**Запускались** шесть проходов. **Не запускался `basics`** — по плану разметки; пропуска темы это
|
||||
не даёт, но означает, что второго провенанса у сигнала о заниженной метке нет.
|
||||
**Не существует в конвейере** проход независимой реализации — снят по стоимости; на задаче,
|
||||
целиком построенной на неподтверждённом внешнем контракте, это самый дорогой пробел прогона.
|
||||
|
||||
**Чего не мог проверить каждый проход:**
|
||||
- `autotests` — качество распознавания гейтом не проверяется вовсе (решение проекта); diff-coverage
|
||||
91%, 4 непокрытых строки — wiring в `cmd/jellybit/serve.go` внутри уже непокрытой функции.
|
||||
- `specs` — границы спеки: пустой primary name при непустом переводе; локализация расширенных
|
||||
данных (объявленный Non-Goal).
|
||||
- `code` — правило про `ident` линтером не проверяется; форма кода вне механизированного перечня.
|
||||
- `architecture` — завязка внешних потребителей на текущее поведение; «нужна ли функциональность».
|
||||
- `adversary` — **три из четырёх вопросов темы `security` не разбирались** (крафт-магнет, снятие
|
||||
последней копии, асимметрия признака владения): вне диффа, бюджет ушёл на построение пути.
|
||||
- `ops` — реальный профиль нагрузки на umbar; замеры сняты в одиночку.
|
||||
- Триаж — ничего нового не находит по определению.
|
||||
|
||||
**Рамка соблюдена:** живых обращений к TheTVDB ноль; `TVDB_API_KEY` не задавался;
|
||||
`TestIntegration_TVDB` не запускался. Оракулы триажа добыты на `httptest`-стенде в копии дерева
|
||||
под `tmp/triage/repo`; рабочее дерево не менялось.
|
||||
|
||||
**Целиком на человеке:** история инцидентов на umbar; поведение SQLite под реальным объёмом;
|
||||
завязка внешних потребителей; «нужна ли функциональность»; качество распознавания.
|
||||
**Перестали проверять сознательно:** идиоматичность Go (проход `idiom` упразднён 2026-08-04).
|
||||
|
||||
**Четыре строки, которые не принесёт ни один проход:** решения проекта (`docs/adr/`) не
|
||||
сверялись — процессный документ; записанные наблюдения (`docs/research/`) как оракул не
|
||||
использовались, и оракул находки 1 заменён на исполненный тест именно поэтому; поимённая сверка с
|
||||
руководствами по стилю Go не задавалась; альтернативной реализации для диффа решений нет.
|
||||
|
||||
**Потолки:** `code` сообщил свой (2 из 4, срез не сработал); `adversary` объявил срез по бюджету;
|
||||
`autotests`, `specs`, `architecture`, `ops` свой потолок не сообщили — это находка о прогоне.
|
||||
Триаж: 6 из 7, потолок не выбран.
|
||||
|
||||
## Что сделано по находкам (записано оркестратором после отработки)
|
||||
|
||||
| № | Действие | Исход |
|
||||
|---|---|---|
|
||||
| 1 | развилка, выбран вариант (а) | Предикат разведён: признаком служит наличие хотя бы одного трёхбуквенного ключа во всей выдаче, а не удача разбора; `null` и `{}` считаются неразобранными. Уровень поднят до `WARN`. Закреплено тестом `TestTVDB_SignalOnUnexpectedTranslationForm` (8 случаев, включая штатный «перевода нет» с ожиданием **отсутствия** следа). В дельте заказана наблюдаемость, уровень отдан конвенциям; добавлены два сценария. |
|
||||
| 2 | развилка, выбран вариант (а) | Утверждение в `design.md` → Risks переписано: сказано, что `plan.Title = match.Title` подставляется после `sanitizePlan`, санитайзинг не применяется, класс пре-существующий, инвариант целевого пути держится. Починка кода **не делалась** — отдана урожаем. |
|
||||
| 3 | инлайн | Выбор ключа детерминирован: точное совпадение сильнее, иначе лексикографически меньший. Тест `TestTVDB_TranslationKeyPickIsDeterministic` (200 прогонов на случай). В дельту добавлено требование о регистронезависимости и детерминизме плюс сценарий «Ключ перевода в другом регистре». |
|
||||
| 4 | инлайн | Добавлен `TestMatchMetadata_TranslationMakesTwoStrong` в `internal/recognize/metadata_test.go`. |
|
||||
| 5 | инлайн | Поле лога переименовано в `tvdb_locale`. |
|
||||
| 6 | инлайн | В `config.example.toml` у `[metadata.tvdb]` дописан хвост про `[general].language` с указанием отличия от TMDB. |
|
||||
|
||||
**Урожай, переданный владельцу задач** (пайплайн задач из него не заводит):
|
||||
|
||||
1. Санитайзинг `match.Title`/`match.Director` при подстановке в план либо категория Cf в
|
||||
`layout.sanitizeComponent` — находка 2, класс общий для TMDB и TVDB, оракул в отчёте.
|
||||
2. Название длиннее ~237 байт уводит задачу в `failed` с текстом системной ошибки вместо review
|
||||
(AD4) — пре-существующее, оракул в отчёте.
|
||||
3. Три promote-кандидата выше: правило синхронности кодов языка, один стенд чужого API на пакет,
|
||||
интеграционный тест обязан утверждать.
|
||||
4. Наблюдение о прогоне: четыре прохода не сообщили свой потолок.
|
||||
5. Наблюдение об окружении: кэш `golangci-lint` в worktree, вложенном в основной репозиторий,
|
||||
нёс результаты чужого прогона с путями `../../internal/…` и красил шаг `lint` находками вне
|
||||
диффа. Лечится `golangci-lint cache clean`. На каждом worktree это ложный красный гейт.
|
||||
Reference in New Issue
Block a user