recognize: название из метабазы санитизируется перед попаданием в план
- чистка стоит на каждой точке входа значения метабазы в план — сборка матча, копия кандидата для ревью, набор закреплённых значений источника и его чтение: гарантия, поставленная только на запись, обходится данными, сохранёнными прежними версиями - название, непригодное как имя каталога (пустое или без единой буквы и цифры), не подставляется — раздача уходит в review с названной причиной - гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям провайдера, чистится только копия, уходящая дальше
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# Значение метабазы чистится на каждой точке входа в план, а три санитайзера не сводятся в один
|
||||
|
||||
- **Дата:** 2026-08-10
|
||||
- **Источник:**
|
||||
[openspec/changes/archive/2026-08-10-metadata-title-sanitize/design.md](../../openspec/changes/archive/2026-08-10-metadata-title-sanitize/design.md),
|
||||
разделы `Decisions` (Решения 1, 1a, 3) и `Non-Goals`
|
||||
|
||||
## Контекст
|
||||
|
||||
Название, приходящее из TMDB/TVDB/TVMaze, попадает в имя каталога библиотеки
|
||||
Jellyfin. Выход LLM мы чистим и считаем недоверенным; название из метабазы того
|
||||
же обращения не получало, хотя приходит так же — из-за периметра. Наблюдаемый
|
||||
исход: каталог из невидимых символов выглядит пустым, кириллическая буква внутри
|
||||
латинского слова даёт вторую папку, неотличимую от первой, и авто-раскладка это
|
||||
пропускала.
|
||||
|
||||
Разбор показал, что точка входа не одна. Их четыре, и каждая ведёт в имя
|
||||
каталога: сборка подтверждённого матча, копия кандидата, уходящая на экран
|
||||
ревью и в хранилище, набор закреплённых значений выбранного человеком
|
||||
источника и **чтение** уже закреплённого значения.
|
||||
|
||||
## Решение
|
||||
|
||||
**Чистка стоит на каждой из четырёх точек, а не в одной «правильной».**
|
||||
|
||||
Цитата из `design.md`, Решение 1a:
|
||||
|
||||
> Закрываются обе одной и той же чисткой, но в трёх местах — по одному на
|
||||
> каждую точку, где значение метабазы входит в домен.
|
||||
|
||||
Плюс четвёртая, добавленная по находке эксплуатационного прохода: чистка **на
|
||||
чтении** закреплённого значения. Гарантия чистоты не может держаться на времени записи строки —
|
||||
кандидаты и закреплённые значения, сохранённые прежними версиями, обходят её,
|
||||
а обычное
|
||||
«Применить» ничего не перезаписывает. Санитайзинг идемпотентен, поэтому лишние
|
||||
точки на уже чистом значении не делают ничего; это же свойство сделано
|
||||
нормативным и покрыто тестом.
|
||||
|
||||
Отдельно: **гейт подтверждения матча чистка не двигает.** Сравнение кандидата с
|
||||
планом идёт по значениям провайдера, чистится только копия, уходящая дальше.
|
||||
Причина в том, что `normalize` и санитайзинг не эквивалентны: невидимый символ
|
||||
внутри слова `normalize` превращает в пробел, а санитайзинг удаляет — чистка до
|
||||
сравнения превратила бы часть нынешних «в review» в «авто».
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
- **Свести три санитайзера проекта в один.** Отвергнуто: у них разный предмет —
|
||||
`recognize.SanitizeTitle` чистит значение, `layout.sanitizeComponent` —
|
||||
компонент пути под требования файловой системы, `naming.sanitize` —
|
||||
отображаемый ярлык. Свёртка гомоглифов — визуально неотличимых букв из разных алфавитов — внутри
|
||||
`sanitizeComponent` сломала бы
|
||||
правило сходимости базы папки: она гоняется и по имени, прочитанному с диска.
|
||||
- **Закрыть только авто-путь, ручной отдать отдельной задаче.** Отвергнуто на
|
||||
чекпоинте: спека `metadata-match` сама называет ручной выбор **основным**
|
||||
путём подтверждения матча — починка коснулась бы менее употребимой половины,
|
||||
а спека утверждала бы свойство, которого нет.
|
||||
- **Чистить в клиентах метабаз.** Отвергнуто: пришлось бы повторять в трёх
|
||||
клиентах и в каждом следующем, а проверка «в плане нет грязных полей»
|
||||
перестала бы читаться в одном месте.
|
||||
- **Разовая правка данных вместо чистки на чтении.** Отвергнута как более
|
||||
дорогая и не закрывающая следующего читателя.
|
||||
- **Полная нормализация Unicode** (NFC/NFKC плюс полная таблица визуально
|
||||
совпадающих символов Unicode) — Non-Goal. Цель — предсказуемое и сверяемое значение, а не исчерпывающая защита
|
||||
от визуального совпадения; курируемая кирилло-латинская таблица закрывает
|
||||
реальный случай.
|
||||
|
||||
## Что осталось нерешённым намеренно
|
||||
|
||||
**Каталог с невидимым символом, уже созданный в библиотеке, кодом не лечится.**
|
||||
Правило сходимости базы папки наследует имя от живой папки-якоря, и очистка
|
||||
извлечённой базы напечатала бы рядом вторую, чистую папку — то есть ровно тот
|
||||
исход с двумя каталогами, против которого затевалось изменение. Лечение —
|
||||
переименовать папку руками, после чего сходимость подхватит новое имя.
|
||||
Изменение закрывает появление новых таких каталогов, а не существующие.
|
||||
|
||||
## Цена
|
||||
|
||||
Точек чистки четыре вместо одной, и правило «значение метабазы чистится на
|
||||
входе в домен» держится на ревью, а не на линтере. Взамен свойство «показанное
|
||||
на экране совпадает с тем, что ляжет на диск» держится устройством кода: чистка
|
||||
стоит в `sourcePins` — общем доме набора закреплённых значений, через который
|
||||
идут и предпросмотр, и закрепление.
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-10 | [Значение метабазы чистится на каждой точке входа в план, три санитайзера не сводятся в один](ADR-2026-08-10-sanitize-at-every-entry.md) | — |
|
||||
| 2026-08-07 | [Локаль TVDB читается из ответа поиска, а не передаётся в запрос](ADR-2026-08-07-tvdb-locale-reads-response.md) | — |
|
||||
| 2026-08-06 | [Спека следует за кодом, когда гарантия недостижима, а окно узкое](ADR-2026-08-06-spec-follows-code-on-narrow-window.md) | — |
|
||||
| 2026-08-04 | [Конвейер ревью и пайплайн задачи переезжают в плагины](ADR-2026-08-04-review-pipeline-to-plugin.md) | — |
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
| Время | `store.Now()` — единственный источник меток времени в данных, всегда UTC; формат хранения — RFC 3339. Вторая санкционированная точка wall-clock — timestamp-часть ULID в `ident.NewID` (исключение `^internal/(ident\|store)/` в `.golangci.yml`). Отдельно от меток в данных стоят замеры длительности: `cmd/jellybit` исключён из `forbidigo` целиком (правило `^cmd/`), плюс точечные `//nolint:forbidigo` в `internal/logging/ext.go` и `internal/httpapi/httpapi.go` |
|
||||
| Идентификаторы | `internal/ident` — генерация и нормализация ULID; `ident.Parse` на каждой входной границе |
|
||||
| Целевые имена и превью раскладки | `internal/naming` — одна логика для превью в UI и для реального применения |
|
||||
| Чистка человекочитаемых значений | три санитайзера с разным предметом, сводить их в один нельзя: `recognize.SanitizeTitle` — значение (недоверенный вход: LLM и метабазы), `layout.sanitizeComponent` — компонент пути под требования ФС, `naming.sanitize` — отображаемый ярлык. Значение метабазы чистится **на каждой** точке входа в план: сборка матча, копия кандидата для ревью, набор закреплённых значений источника и его чтение — [ADR-2026-08-10-sanitize-at-every-entry](adr/ADR-2026-08-10-sanitize-at-every-entry.md) |
|
||||
| Разбор источника | `internal/magnet` и `internal/torrent`; инфохэш извлекается только здесь |
|
||||
| Приём | use-case `ingest` — общий путь для HTTP, веб-UI, Telegram и CLI |
|
||||
| Переходы состояний | `worker` под per-download блокировкой; легальность перехода задаётся декларативным графом |
|
||||
|
||||
@@ -181,6 +181,10 @@ Go-сервиса и что здесь уже проскакивало. Устр
|
||||
- `security`: читается ли тело ответа внешнего сервиса целиком без предела —
|
||||
лимита на размер ответа LLM в проекте нет, и это единственный недоверенный
|
||||
канал, где предел не стоит ([security.md](security.md) → «Что вне модели»)
|
||||
- `operations`: гарантия, которую вводит изменение, поставлена на запись или на
|
||||
чтение — и что будет с данными, записанными до деплоя, которые обычный путь
|
||||
не перезаписывает? (журнал, 2026-08-10: чистка названия стояла на записи, и
|
||||
очередь ревью её обходила)
|
||||
- `operations`: не удваивает ли новая ветка расход лимита метабаз и платного
|
||||
LLM — повтор, ретрай, «распознать заново» на том же входе? (кэша ответов нет,
|
||||
задача `metadata-cache`)
|
||||
@@ -316,6 +320,44 @@ Go-сервиса и что здесь уже проскакивало. Устр
|
||||
случаи до этой даты не восстанавливались — восстановленная постфактум причина
|
||||
непоймания недостоверна, а именно она и нужна.
|
||||
|
||||
## 2026-08-10 — чистка названия метабазы стояла только на записи, и очередь ревью её обходила [пойман]
|
||||
|
||||
- **Где:** `internal/worker/review.go` — `sourcePins`, `applyOverrides`,
|
||||
`buildSources`. Норма — `openspec/specs/metadata-match/spec.md`, требование
|
||||
«Санитайзинг названий кандидатов, уходящих в ревью», и
|
||||
`openspec/specs/review/spec.md`, «Подтверждение матча обновляет отображаемое
|
||||
имя».
|
||||
- **Симптом:** найден на ревью самой задачи `metadata-title-sanitize`, до
|
||||
мерджа. В эксплуатации не всплывал. Сошлись независимо четыре прохода:
|
||||
`adversary` (построенный путь с падающим тестом), `ops` (постмортем),
|
||||
`specs` и `code`.
|
||||
- **Причина:** правка чистила значение метабазы **в момент записи** — при
|
||||
копировании кандидата в список для ревью. Из этого следовали три дыры разом.
|
||||
(1) Кандидаты, сохранённые прежними версиями, лежат в хранилище грязными, а
|
||||
их выбор человеком закреплял название дословно. (2) `applyOverrides` читал
|
||||
значение, закреплённое до деплоя, дословно, и обычное «Применить» без
|
||||
повторного выбора источника создавало ровно тот каталог, ради которого
|
||||
правка затевалась. (3) Предпросмотр источника на экране считался из сырого
|
||||
названия, а гейт пригодности стоял только на закреплении — экран показывал
|
||||
одно, раскладка делала другое, при том что «превью = применение» записано
|
||||
требованием `web-ui`.
|
||||
- **Чем воспроизведён:** тремя тестами, каждый падает без правки (проверено
|
||||
прогоном с временно снятой правкой): `TestBuildSources_PreviewMatchesApply`,
|
||||
`TestChooseCandidate_DirtyLegacyTitleSanitized`,
|
||||
`TestApplyOverrides_LegacyDirtyPinSanitized`. Плюс прогон `adversary`:
|
||||
превью `- (2014) [tvdbid-269613]/…` против применяемого
|
||||
`Догадка (2014) [tvdbid-269613]/…` на одном экране.
|
||||
- **Почему не поймали:** ловить было нечему — дефект поймали на этом же ревью,
|
||||
до мерджа. Записывается ради причины его появления: **гарантия, поставленная
|
||||
на запись, молчаливо не распространяется на данные, записанные раньше.**
|
||||
Ревью дизайна дошло до «закрыть оба пути подтверждения матча», но точкой
|
||||
закрытия выбрало запись, а не чтение; вопрос «а что с тем, что уже лежит в
|
||||
хранилище» не задал никто из трёх проходов стадии дизайна. Его задал
|
||||
эксплуатационный проход — на оси времени, где он и живёт.
|
||||
- **Что меняем:** вопрос темы `operations` (ниже) — про гарантию, поставленную
|
||||
на запись. Решение по существу — ADR-2026-08-10-sanitize-at-every-entry:
|
||||
чистка стоит на каждой точке входа, включая чтение.
|
||||
|
||||
## 2026-08-06 — уборка своего торрента после отмены сносит чужие файлы [проскочил]
|
||||
|
||||
- **Где:** `internal/worker/worker.go:501-556` — гард `:501-509`, удаление
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ REST API работают **без авторизации** осознанно;
|
||||
| Текстовый контекст человека | все транспорты | попадает в промпт LLM целиком |
|
||||
| Сообщение торрент-бота | Telegram (пересылка) | чужой формат, парсер, ссылки; текст автора бота, а не отправителя |
|
||||
| **Ответ LLM** | HTTP к эндпоинту | целиком под влиянием входа выше; названия, годы, номера сезонов и серий, из которых строится целевой путь |
|
||||
| Ответы метабаз | HTTP к TMDB/TVDB/TVMaze | канонические названия, из которых тоже строится путь |
|
||||
| Ответы метабаз | HTTP к TMDB/TVDB/TVMaze | канонические названия, из которых тоже строится путь; чистятся наравне с выходом LLM на каждой точке входа в план ([ADR-2026-08-10-sanitize-at-every-entry](adr/ADR-2026-08-10-sanitize-at-every-entry.md)) |
|
||||
| Ответы qBittorrent | HTTP | пути, состояния, размеры |
|
||||
| Запросы веб-UI и REST | LAN | идентификаторы, параметры действий |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user