Files
jellybit/openspec/changes/archive/2026-08-10-metadata-title-sanitize/tasks.md
T
av 9aecf757e0 recognize: название из метабазы санитизируется перед попаданием в план
- чистка стоит на каждой точке входа значения метабазы в план — сборка матча,
  копия кандидата для ревью, набор закреплённых значений источника и его
  чтение: гарантия, поставленная только на запись, обходится данными,
  сохранёнными прежними версиями
- название, непригодное как имя каталога (пустое или без единой буквы и
  цифры), не подставляется — раздача уходит в review с названной причиной
- гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям
  провайдера, чистится только копия, уходящая дальше
2026-08-10 10:41:16 +03:00

8.0 KiB
Raw Blame History

1. Код

  • 1.1 В internal/recognize/recognize.go прогнать match.Title через sanitizeTitle перед подстановкой в plan.Title; год, режиссёр и провайдер оставить как есть
  • 1.2 Непригодный результат чистки (пусто либо ни одной буквы и цифры): подстановку не выполнять, прежнее название сохранить, причину передать в decide, а не дописывать к готовому решению (см. design, Решение 4)
  • 1.3 Логировать на Debug, когда чистка реально изменила каноническое название — тем же способом, что sanitizeField (названия не секреты)
  • 1.4 Чистить Title/OriginalTitle кандидата на копии, в момент append в candidates (internal/recognize/metadata.go). Место накопления не двигать — оно стоит до if match != nil { continue }, и перенос обеднил бы список кандидатов при подтверждённом матче с заблокированным авто. Срез cands не мутировать: for _, c := range cands даёт копию структуры (все поля metadata.Candidate скалярные), поэтому сравнение остаётся на значениях провайдера и гейт матча не сдвигается
  • 1.5 Закрепление кандидата с непригодным названием (chooseCandidateLocked, internal/worker/review.go): пин названия не ставится, в плане остаётся название распознавания — тот же исход, что у канонического названия

2. Тесты

  • 2.1 Табличный тест на четыре входа из «Воспроизведения» задачи: три ZWSP (U+200B), Dune<RLO>gnp.mkv (U+202E), Dune\nHACK, Dunа с кириллической а — проверяет plan.Title и Decision.Auto
  • 2.2 Граничные случаи непригодного названия: три ZWSP (пусто после чистки) и вырожденные ".", "...", "-", " . " — план сохраняет прежнее название, Auto=false, причина названа, год и провайдер подставлены
  • 2.5 Идемпотентность: sanitizeTitle(sanitizeTitle(x)) == sanitizeTitle(x) по тому же набору входов, что и 2.1
  • 2.6 Кандидаты, уходящие в ревью, очищены: те же четыре входа в названии кандидата — в Result.Candidates значения санитизированы
  • 2.7 Гейт матча не сдвинулся: кандидат, отличающийся от плана только zero-width внутри слова, по-прежнему не даёт подтверждённого матча (сравнение идёт по значению провайдера)
  • 2.8 Список кандидатов не обеднел: при подтверждённом матче от первого провайдера кандидаты остальных провайдеров того же ключа по-прежнему в списке
  • 2.9 Закрепление кандидата с названием -: пин названия не поставлен, в плане остаётся название распознавания, каталог с мусорным именем не строится
  • 2.3 Нормальное название матча: значение в плане совпадает с ответом базы, Auto=true при прочих чистых условиях
  • 2.4 Прогнать существующие тесты internal/recognize и internal/metadata без правки ожиданий — регресс на TMDB виден сразу

3. Гейт и спеки

  • 3.1 task gate зелёный
  • 3.2 openspec validate --strict metadata-title-sanitize

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

Приходят из постановки tasks/items/metadata-title-sanitize.md, здесь — дословно.

  • A1 Все четыре входа из «Воспроизведения» дают либо санитизированный plan.Title, либо уход в review — но не авто-раскладку с исходным значением. Оракул: тот самый падающий тест из отчёта триажа, перенесённый в дерево
  • A2 Название, схлопывающееся санитизацией в пустую строку, не порождает каталог с пустым именем и не роняет раскладку. Оракул: табличный тест на границе, случай «три ZWSP»
  • A3 Поведение TMDB на нормальных названиях не изменилось. Оракул: существующие тесты internal/recognize и internal/metadata зелёные без правок ожиданий
  • A4 Место санитизации названо требованием спеки, а не только кодом. Оракул: openspec validate --strict на дельте

4. Правки по ревью кода

  • 4.1 Санитайзинг канонического названия перенесён в buildMatch — единственную точку сборки Match; recognize.go и decide читают уже чистое значение и не пересчитывают условие каждый по-своему
  • 4.2 Чистка и проверка пригодности названия источника перенесены в sourcePins — общий дом набора пинов, через который идут и превью, и закрепление; «превью = применение» держится конструкцией
  • 4.3 Кандидаты, сохранённые прежней версией, чистятся на закреплении: гарантия не держится на времени записи строки
  • 4.4 Отказ закрепить непригодное название виден в журнале — атрибут title_pinned у существующей записи review candidate chosen
  • 4.5 Дельта review — требование «Подтверждение матча обновляет отображаемое имя» дополнено санитайзингом на закреплении и свойством «превью = применение»
  • 4.6 Тесты: TestChooseCandidate_DirtyLegacyTitleSanitized, TestBuildSources_PreviewMatchesApply (оба падают без правки)
  • 4.7 Чистка названия и на чтении пина (applyOverrides): пин, закреплённый прежней версией, обычным «Применить» не переписывается, и без этого доезжал до имени каталога дословно (находка эксплуатационного прохода)
  • 4.8 naming.sanitize снимает категорию Cf: дельта recognition обосновывает отказ чистить director в плане тем, что его чистит рендер, — а рендер снимал только C0, и RLO из credits разворачивал карточку Telegram (находка триажа)