recognize: название из метабазы санитизируется перед попаданием в план

- чистка стоит на каждой точке входа значения метабазы в план — сборка матча,
  копия кандидата для ревью, набор закреплённых значений источника и его
  чтение: гарантия, поставленная только на запись, обходится данными,
  сохранёнными прежними версиями
- название, непригодное как имя каталога (пустое или без единой буквы и
  цифры), не подставляется — раздача уходит в review с названной причиной
- гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям
  провайдера, чистится только копия, уходящая дальше
This commit is contained in:
av
2026-08-10 10:41:16 +03:00
parent bb278e8744
commit 9aecf757e0
26 changed files with 1653 additions and 53 deletions
+8 -2
View File
@@ -20,6 +20,7 @@ import (
"log/slog"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"git.vakhrushev.me/av/jellybit/internal/llm"
@@ -202,13 +203,18 @@ func render(f Fields) string {
return Label(f.Title, f.Director, f.Year, f.SeasonLabel())
}
// sanitize убирает управляющие символы и переводы строк, схлопывает пробелы.
// sanitize убирает управляющие и невидимые символы, переводы строк, схлопывает
// пробелы. Категорию Cf (zero-width, BOM, RLO) снимаем наравне с C0: значения
// приходят из метабазы и из сохранённого контекста, то есть снаружи, а ярлык
// уезжает в карточку Telegram, в шапку веб-UI и в имя раздачи qBittorrent —
// RLO там переворачивает отображение. Это же обещает требование metadata-match:
// режиссёр в плане не чистится именно потому, что его чистит рендер.
func sanitize(s string) string {
s = strings.Map(func(r rune) rune {
if r == '\n' || r == '\t' || r == '\r' {
return ' '
}
if r < 0x20 {
if r < 0x20 || unicode.Is(unicode.Cf, r) {
return -1
}
return r
+22
View File
@@ -158,3 +158,25 @@ func TestDeriveNameNilProviderUsesFallback(t *testing.T) {
t.Errorf("DeriveName() = %q (ожидался фолбек без LLM)", got)
}
}
// Значения ярлыка приходят снаружи — из credits метабазы и из сохранённого
// контекста. Категория Cf (zero-width, BOM, RLO) обязана сниматься здесь:
// требование metadata-match не чистит режиссёра в плане именно потому, что его
// чистит рендер. RLO в карточке Telegram переворачивает отображение имени.
func TestSanitize_StripsFormatChars(t *testing.T) {
cases := []struct{ in, want string }{
{"Denis\u202eVilleneuve", "DenisVilleneuve"},
{"Denis\u200bVilleneuve", "DenisVilleneuve"},
{"Denis\ufeffVilleneuve", "DenisVilleneuve"},
{"Denis\nVilleneuve", "Denis Villeneuve"},
{"Дени Вильнёв", "Дени Вильнёв"},
}
for _, c := range cases {
if got := sanitize(c.in); got != c.want {
t.Errorf("sanitize(%q) = %q, want %q", c.in, got, c.want)
}
}
if got := Label("Dune", "Denis\u202eVilleneuve", 2021, ""); got != "Dune (DenisVilleneuve, 2021)" {
t.Errorf("Label = %q", got)
}
}