- локаль из [general].language применяется при разборе ответа /search, а в запрос не уходит: параметр language у TVDB — фильтр выдачи, а не селектор перевода (ADR-2026-08-07) - Title берётся из блока translations с тотальным фолбэком на primary name, OriginalTitle — из primary name; форма ответа сверена по документации и живым прогоном не подтверждена (docs/research) - неожиданная форма ответа даёт WARN: признак — отсутствие во всей выдаче ключей языка ожидаемого вида, а не неудача разбора блока
23 KiB
Ревью изменения 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 асимметрия была верна. - Действие: инлайн
Гипотезы без доказательства
- Достижимость находки 3 (два EqualFold-равных ключа от живого TVDB) не проверена и в рамке прогона проверена быть не может.
- Основание всего изменения — форма блока
translations— живым прогоном не подтверждено. Все находки про перевод условны на этом. Единственный оракул —TestIntegration_TVDBзаTVDB_API_KEY, прогоняет человек. Отдельно отspecs: интеграционный тест печатает результат поNe Zha, но ничего не утверждает —PASSс пустым выводом неотличим от подтверждения. - AD4 (название длиннее ~237 байт уводит задачу в
failedвместо review) понижено не по доказательности (оракул есть:n=250→Apply err=… file name too long,results=0), а по принадлежности к диффу: путь не тронут изменением. - Согласие пяти проходов по находке 1 — не подтверждение.
Confidence: highстоит из-за падающего теста, добытого триажом, и ни по какой другой причине.
Promote candidates
- Правило
internal/archrules, связывающее множество кодов языка с мапперами (провенансarchitecture/R2). Язык вывода живёт в пяти местах четырёх пакетов; тестов, связывающих множество с мапперами, нет — забытая ветка молча даст английский вывод. Дешёвый паллиатив: строка «язык вывода» вdocs/architecture.md→ «Единые точки проекта». - Один стенд чужого API на пакет (провенанс
architecture/R3). Вtvdb_test.goдва фейка одного API; когда форма ответа поменяется по факту разведки, забытыйfakeTVDBостанется зелёным и продолжит подтверждать опровергнутое предположение. - Интеграционный тест обязан утверждать, а не печатать (провенанс
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. |
Урожай, переданный владельцу задач (пайплайн задач из него не заводит):
- Санитайзинг
match.Title/match.Directorпри подстановке в план либо категория Cf вlayout.sanitizeComponent— находка 2, класс общий для TMDB и TVDB, оракул в отчёте. - Название длиннее ~237 байт уводит задачу в
failedс текстом системной ошибки вместо review (AD4) — пре-существующее, оракул в отчёте. - Три promote-кандидата выше: правило синхронности кодов языка, один стенд чужого API на пакет, интеграционный тест обязан утверждать.
- Наблюдение о прогоне: четыре прохода не сообщили свой потолок.
- Наблюдение об окружении: кэш
golangci-lintв worktree, вложенном в основной репозиторий, нёс результаты чужого прогона с путями../../internal/…и красил шагlintнаходками вне диффа. Лечитсяgolangci-lint cache clean. На каждом worktree это ложный красный гейт.