recognize: название из метабазы санитизируется перед попаданием в план
- чистка стоит на каждой точке входа значения метабазы в план — сборка матча, копия кандидата для ревью, набор закреплённых значений источника и его чтение: гарантия, поставленная только на запись, обходится данными, сохранёнными прежними версиями - название, непригодное как имя каталога (пустое или без единой буквы и цифры), не подставляется — раздача уходит в review с названной причиной - гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям провайдера, чистится только копия, уходящая дальше
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Санитайзинг названий кандидатов, уходящих в ревью
|
||||
|
||||
Система SHALL санитизировать названия кандидата (`Title`, `OriginalTitle`) тем же
|
||||
санитайзингом человекочитаемых полей плана (см. `recognition`) перед тем, как
|
||||
унести их из сверки дальше: в список кандидатов для ревью, в хранилище, на экран
|
||||
и в закрепляемое человеком значение. Причина та же, что у
|
||||
канонического названия: выбор кандидата человеком — полноправный путь
|
||||
подтверждения матча, и закреплённое им название становится именем каталога
|
||||
библиотеки в тех же условиях, что и название авто-матча.
|
||||
|
||||
Условия подтверждения сильного матча система SHALL проверять на значениях, как их
|
||||
отдал провайдер: санитайзинг кандидатов SHALL NOT влиять на эти значения. Порядок
|
||||
операций требование не нормирует — нормирует исход: гейт матча этой правкой не
|
||||
двигается, иначе кандидат, чьё название отличается от плана невидимым символом,
|
||||
начал бы совпадать там, где прежде уходил в review. Полнота списка кандидатов
|
||||
тоже SHALL остаться прежней.
|
||||
|
||||
Если название кандидата после санитайзинга непригодно как компонент пути (пусто
|
||||
либо без единой буквы и цифры), система SHALL сохранить кандидата в списке — он
|
||||
остаётся выбором человека и несёт `provider_id` и URL для внешней проверки, — но
|
||||
закрепление такого названия SHALL приводить к тому же исходу, что и у
|
||||
канонического: подстановки не происходит, в плане остаётся название
|
||||
распознавания.
|
||||
|
||||
`OriginalTitle` кандидата чистится наравне с `Title`, хотя в хранилище и на экран
|
||||
сегодня доходит только второе: первое уезжает в результат распознавания и в
|
||||
диагностику сухого прогона, и держать в одной структуре одно чистое поле и одно
|
||||
грязное — источник будущей ошибки.
|
||||
|
||||
#### Scenario: Название кандидата чистится перед показом и закреплением
|
||||
|
||||
- **GIVEN** провайдер вернул кандидата, название которого содержит zero-width
|
||||
символ и кириллический двойник внутри латинского слова
|
||||
- **WHEN** кандидат попадает в список для ревью
|
||||
- **THEN** его название очищено тем же санитайзингом, что и поля плана
|
||||
- **AND** человек, выбравший этого кандидата, закрепляет очищенное название
|
||||
- **AND** в имя каталога библиотеки оно уходит в тех же условиях, что и название
|
||||
авто-матча (живой папки-якоря того же тайтла нет — см. `file-layout`,
|
||||
«Сходимость базы папки при подтверждённом матче»)
|
||||
|
||||
#### Scenario: Сравнение с планом идёт по значению провайдера
|
||||
|
||||
- **GIVEN** кандидат, название которого отличается от названия плана только
|
||||
невидимым символом внутри слова
|
||||
- **WHEN** проверяются условия подтверждения сильного матча
|
||||
- **THEN** сравнение идёт по значению, как его отдал провайдер
|
||||
- **AND** исход подтверждения матча тот же, что был до этого изменения
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Подтверждение матча и каноническое имя
|
||||
|
||||
При единичном сильном матче система SHALL брать из записи базы официальный
|
||||
`provider` (`tmdb`|`tvdb`|`tvmaze`) и `provider_id`, а также каноническое название
|
||||
и год, и подменять ими соответствующие поля плана (для сериала — с учётом внешнего
|
||||
тега TVDB/IMDb из `externals`, идущего в имя папки). Матч SHALL считаться
|
||||
подтверждённым только при ровно одном сильном кандидате; при нуле или нескольких
|
||||
кандидатах подтверждённого матча быть SHALL NOT (авто-раскладка не разрешается,
|
||||
кандидаты уходят в review). Работа с базами опциональна: при выключенных базах
|
||||
сверка не выполняется и подтверждённого матча нет.
|
||||
|
||||
Каноническое название — **недоверенное значение внешнего сервиса**, и перед
|
||||
подстановкой в план система SHALL применять к нему тот же санитайзинг
|
||||
человекочитаемых полей, что и к выводу LLM (см. `recognition`, требование
|
||||
«Санитайзинг человекочитаемых полей плана»). Подстановка несанитизированного
|
||||
значения SHALL NOT выполняться: план — источник имени каталога библиотеки, и
|
||||
значение, не прошедшее чистку, уходит в путь на диске.
|
||||
|
||||
Если после санитайзинга каноническое название оказывается **непригодным как
|
||||
компонент пути** — пустым либо вырожденным, то есть не содержащим ни одной буквы
|
||||
и ни одной цифры (`.`, `..`, `-`, только пунктуация), — система SHALL сохранить в
|
||||
плане прежнее название и SHALL NOT разрешать авто-раскладку: причина уходит в
|
||||
перечень причин решения, раздача попадает в review. Проверка пригодности SHALL
|
||||
стоять **после** санитайзинга, а не до него. Ронять распознавание или раскладку
|
||||
такой матч SHALL NOT — год, провайдер и `provider_id` при этом подставляются как
|
||||
обычно.
|
||||
|
||||
Санитайзинг, **изменивший** каноническое название, но оставивший его пригодным,
|
||||
авто-раскладку SHALL NOT блокировать: в план идёт очищенное значение, и оно
|
||||
предсказуемо — ради этого чистка и стоит.
|
||||
|
||||
При подтверждённом матче система SHALL дополнительно попытаться получить из базы
|
||||
**режиссёра** (TMDB/TVDB credits) и вложить его в план (`director`) как
|
||||
недоверенное косметическое значение для вывода отображаемого имени. Тот же способ
|
||||
выборки режиссёра по `provider:id` SHALL быть доступен при закреплении вручную
|
||||
выбранного в ревью кандидата (см. `review`), т.к. основной путь подтверждения
|
||||
матча — ручной выбор, а не авто. Выборка режиссёра SHALL быть best-effort: её
|
||||
недоступность, отсутствие в базе или провайдер без режиссёра (напр. TVMaze) SHALL
|
||||
NOT проваливать распознавание/матч/выбор — `director` остаётся пустым, а имя
|
||||
выводится без режиссёра или из более низкого слоя (сохранённый контекст). Режиссёр
|
||||
из метабазы SHALL иметь приоритет над режиссёром из контекста (более проверенный
|
||||
источник).
|
||||
|
||||
Режиссёр — недоверенное человекочитаемое поле: он SHALL NOT участвовать в
|
||||
структурной валидации/гейте авто-раскладки, а его очистка (управляющие символы,
|
||||
пробелы, лимит длины) применяется при рендере отображаемого имени, а не в
|
||||
plan-санитайзинге.
|
||||
|
||||
#### Scenario: Единичный матч даёт id и каноническое имя
|
||||
|
||||
- **GIVEN** поиск вернул ровно одного сильного кандидата TMDB для фильма
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** план получает `provider`=`tmdb`, `provider_id`, каноническое название и год
|
||||
|
||||
#### Scenario: Каноническое название чистится перед подстановкой
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч, каноническое название которого содержит
|
||||
zero-width символ, перевод строки или кириллический двойник внутри латинского слова
|
||||
- **WHEN** каноническое название подставляется в план
|
||||
- **THEN** в плане оказывается санитизированное значение
|
||||
- **AND** авто-раскладка остаётся разрешённой, если прочие условия выполнены
|
||||
- **AND** когда база имени папки печатается из распознавания (живой папки-якоря
|
||||
того же тайтла нет — см. `file-layout`, «Сходимость базы папки при подтверждённом
|
||||
матче»), в имя каталога уходит очищенное значение
|
||||
|
||||
#### Scenario: Название базы непригодно как компонент пути — раздача уходит в review
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч, каноническое название которого после
|
||||
санитайзинга пусто (целиком состояло из zero-width символов) либо не содержит ни
|
||||
одной буквы и ни одной цифры (`.`, `...`, `-`)
|
||||
- **WHEN** каноническое название подставляется в план
|
||||
- **THEN** название плана остаётся прежним (подстановки не происходит)
|
||||
- **AND** решение auto/review содержит причину «название из базы непригодно как имя
|
||||
каталога», авто-раскладка не разрешается
|
||||
- **AND** распознавание не проваливается, год и провайдер подставлены
|
||||
|
||||
#### Scenario: Нормальное название матча не меняется
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч с обычным каноническим названием
|
||||
- **WHEN** каноническое название подставляется в план
|
||||
- **THEN** значение в плане совпадает с тем, что отдала база (санитайзинг на чистом
|
||||
значении ничего не меняет)
|
||||
- **AND** авто-раскладка остаётся разрешённой, если прочие условия выполнены
|
||||
|
||||
#### Scenario: Матч подтягивает режиссёра
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч TMDB для фильма, у которого в credits
|
||||
указан режиссёр
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** в план вкладывается `director` из credits
|
||||
- **AND** отображаемое имя может использовать этого режиссёра
|
||||
|
||||
#### Scenario: Режиссёр недоступен — матч не ломается
|
||||
|
||||
- **GIVEN** подтверждённый матч, для которого выборка режиссёра недоступна или
|
||||
провайдер режиссёра не отдаёт
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** `director` остаётся пустым
|
||||
- **AND** матч подтверждён, распознавание не проваливается
|
||||
|
||||
#### Scenario: Несколько кандидатов — матч не подтверждён
|
||||
|
||||
- **GIVEN** поиск вернул более одного подходящего кандидата
|
||||
- **WHEN** оценивается матч
|
||||
- **THEN** подтверждённого матча нет, кандидаты собираются для выбора в review
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Санитайзинг человекочитаемых полей плана
|
||||
|
||||
Перед структурной валидацией плана и сверкой с базами система SHALL санитизировать
|
||||
человекочитаемые поля плана — `title`, `original_title`, `provider_hint` — как
|
||||
недоверенный вывод LLM. Санитайзинг SHALL: (1) удалять управляющие и zero-width
|
||||
символы (C0/C1, `U+200B` и родственные, BOM `U+FEFF`); (2) сводить внутренние
|
||||
последовательности пробельных к одиночному пробелу и обрезать края; (3) сворачивать
|
||||
кирилло-латинские homoglyph-двойники (см. ниже).
|
||||
|
||||
Санитайзинг SHALL быть **общим для обоих источников названия** — вывода LLM и
|
||||
записи метабазы. Значение, пришедшее из метабазы и заменяющее поле плана при
|
||||
подтверждённом матче, SHALL проходить тот же санитайзинг перед подстановкой (см.
|
||||
`metadata-match`, требование «Подтверждение матча и каноническое имя»). Второго
|
||||
способа чистить **названия плана** система SHALL NOT заводить: одна реализация
|
||||
обслуживает оба источника. Речь только о полях плана — очистка компонента пути
|
||||
под требования файловой системы (`layout`) и очистка отображаемого ярлыка
|
||||
(`naming`) остаются своими, отдельными и законными.
|
||||
|
||||
Санитайзинг SHALL быть **идемпотентным**: повторное применение к уже
|
||||
санитизированному значению SHALL NOT изменять его. На этом свойстве стоит
|
||||
утверждение, что правка не меняет путей на диске для раздач с нормальным
|
||||
названием, и оно проверяется тестом, а не подразумевается.
|
||||
|
||||
Следствие, на которое опирается раскладка: к моменту, когда план уходит в решение
|
||||
auto/review, каждое из полей `title`, `original_title`, `provider_hint` совпадает
|
||||
с тем, что дал бы санитайзинг этого значения — независимо от того, пришло оно от
|
||||
LLM или из метабазы.
|
||||
|
||||
Свёртка двойников SHALL работать потокенно (по словам, разделённым не-буквенными
|
||||
символами): токен, все буквы которого принадлежат одному скрипту, система SHALL
|
||||
оставлять без изменений (билингвальность реальна — кириллические названия
|
||||
неприкосновенны); в токене смешанного скрипта система SHALL определять доминирующий
|
||||
скрипт по числу буквенных рун и заменять буквы-меньшинство их визуальными
|
||||
двойниками из доминирующего скрипта по курируемой таблице. При отсутствии
|
||||
доминирующего скрипта (равенство) токен SHALL оставаться без изменений.
|
||||
|
||||
Санитайзинг SHALL применяться ТОЛЬКО к перечисленным человекочитаемым полям
|
||||
**плана**. Применение того же санитайзинга к значениям, пришедшим из метабазы —
|
||||
каноническому названию и названиям кандидатов, — заказано отдельно, см.
|
||||
`metadata-match`.
|
||||
`files[].src` система SHALL NOT санитизировать — эти значения обязаны совпадать с
|
||||
реальными файлами торрента, и расхождение (в т.ч. homoglyph) SHALL оставаться
|
||||
основанием отклонить план, а не поводом «чинить» путь. `director` система SHALL
|
||||
NOT санитизировать в плане — он не участвует в пути на диске, и его очистка
|
||||
применяется при рендере отображаемого имени (см. `metadata-match`).
|
||||
|
||||
Когда санитайзинг реально изменил значение поля, система SHALL логировать это на
|
||||
уровне `Debug` (названия не относятся к секретам).
|
||||
|
||||
#### Scenario: Кириллический двойник в англоязычном названии сворачивается
|
||||
|
||||
- **GIVEN** план, где `title` = `Hаrold and the Purple Crayon` (буква `а` в первом
|
||||
слове — кириллическая `U+0430`)
|
||||
- **WHEN** план санитизируется
|
||||
- **THEN** первое слово становится `Harold` (все буквы латинские)
|
||||
- **AND** в запрос к базе и в сравнение уходит латинское название
|
||||
|
||||
#### Scenario: Честное кириллическое название не трогается
|
||||
|
||||
- **GIVEN** план российского фильма с `title` = `Тёмный рыцарь`, где все буквы
|
||||
каждого слова кириллические
|
||||
- **WHEN** план санитизируется
|
||||
- **THEN** название остаётся кириллическим без замены букв
|
||||
|
||||
#### Scenario: Токен без доминирующего скрипта не трогается
|
||||
|
||||
- **GIVEN** план, где короткий токен содержит поровну латинских и кириллических
|
||||
букв (доминирующего скрипта нет)
|
||||
- **WHEN** план санитизируется
|
||||
- **THEN** этот токен остаётся без замены букв (осознанный trade-off: двухбуквенный
|
||||
homoglyph-typo не сворачивается)
|
||||
|
||||
#### Scenario: Zero-width и лишние пробелы вычищаются
|
||||
|
||||
- **GIVEN** план, где `title` содержит zero-width символ и сдвоенные пробелы
|
||||
- **WHEN** план санитизируется
|
||||
- **THEN** zero-width удалён, внутренние пробелы сведены к одиночным, края обрезаны
|
||||
|
||||
#### Scenario: files[].src не санитизируется
|
||||
|
||||
- **GIVEN** ответ LLM, где `files[].src` содержит символ-двойник и не совпадает ни
|
||||
с одним реальным файлом торрента
|
||||
- **WHEN** план обрабатывается
|
||||
- **THEN** `files[].src` НЕ изменяется санитайзингом
|
||||
- **AND** несовпадение src приводит к отклонению плана (эскалация в review), а не к
|
||||
«починке» пути
|
||||
|
||||
#### Scenario: Название из метабазы чистится тем же санитайзингом
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч, каноническое название которого содержит
|
||||
zero-width символ и кириллический двойник внутри латинского слова
|
||||
- **WHEN** каноническое название подставляется в план
|
||||
- **THEN** в плане оказывается значение, прошедшее тот же санитайзинг, что и вывод
|
||||
LLM
|
||||
|
||||
#### Scenario: После сверки с базой несанитизированных полей в плане не остаётся
|
||||
|
||||
- **GIVEN** план после подтверждённого матча с базой
|
||||
- **WHEN** оценивается решение auto/review
|
||||
- **THEN** каждое из полей `title`, `original_title`, `provider_hint` совпадает с
|
||||
тем, что дал бы санитайзинг этого значения
|
||||
- **AND** `director` под это требование не подпадает: он чистится при рендере
|
||||
отображаемого имени
|
||||
|
||||
#### Scenario: Повторный санитайзинг ничего не меняет
|
||||
|
||||
- **GIVEN** значение, уже прошедшее санитайзинг
|
||||
- **WHEN** санитайзинг применяется к нему второй раз
|
||||
- **THEN** значение не изменяется
|
||||
@@ -0,0 +1,69 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Подтверждение матча обновляет отображаемое имя
|
||||
|
||||
Система SHALL при подтверждении матча в ревью запускать обновление отображаемого
|
||||
имени загрузки по подтверждённому распознаванию (см. capability `ingest`):
|
||||
переливать **полный ярлык** имени — «Название (режиссёр, год)», для сериала со
|
||||
сводкой сезонов — в `download.display_name` и в имя раздачи qBittorrent, без
|
||||
нового вызова LLM. Имя строится из эффективных полей (override → распознавание с
|
||||
вложенным матчем → сохранённый контекст). Подтверждением матча SHALL
|
||||
считаться как выбор кандидата из списка совпадений, так и ручное добавление
|
||||
источника по id/URL (оба закрепляют провайдера и каноническое название).
|
||||
|
||||
Закрепляемое название источника SHALL проходить санитайзинг человекочитаемых
|
||||
полей (см. `recognition`) и проверку пригодности как компонента пути (см.
|
||||
`metadata-match`) — на **общей** точке сборки набора пинов источника, той же,
|
||||
через которую строится предпросмотр. Отсюда следует свойство, на которое
|
||||
опирается экран ревью: показанное для источника название и путь совпадают с тем,
|
||||
что закрепится и разложится по выбору этого источника. Название, непригодное как
|
||||
имя каталога, пином SHALL NOT становиться — в плане остаётся название
|
||||
распознавания, а факт отказа SHALL быть наблюдаем в журнале.
|
||||
|
||||
Санитайзинг на закреплении SHALL применяться независимо от того, было ли значение
|
||||
очищено при сохранении кандидата: гарантия чистоты не может держаться на времени
|
||||
записи строки, иначе кандидаты, сохранённые прежними версиями, обходят её. Тот же
|
||||
санитайзинг идемпотентен, поэтому на уже очищенном значении он ничего не меняет.
|
||||
|
||||
При закреплении выбранного/добавленного источника система SHALL best-effort
|
||||
получить режиссёра этого источника из метабазы (credits по `provider:id`, см.
|
||||
`metadata-match`) и закрепить его как override, чтобы он попал в эффективные поля
|
||||
и в ярлык. Недоступность credits или отсутствие режиссёра SHALL NOT проваливать
|
||||
выбор источника: режиссёр остаётся из более низкого слоя (сохранённый контекст)
|
||||
или пустым. Так режиссёр из метабазы появляется и на **основном** пути
|
||||
подтверждения — ручном выборе кандидата, а не только при авто-матче.
|
||||
|
||||
Обновление SHALL выполняться после успешного закрепления выбора кандидата и
|
||||
SHALL быть best-effort по отношению к qBittorrent: недоступность клиента SHALL
|
||||
NOT проваливать команду ревью. Это согласуется с инвариантом «авто-действие
|
||||
только при подтверждённом матче».
|
||||
|
||||
#### Scenario: Выбор кандидата переливает каноническое имя
|
||||
|
||||
- **GIVEN** загрузка в ревью с кандидатами метабазы
|
||||
- **WHEN** человек выбирает кандидата
|
||||
- **THEN** провайдер, id и каноническое название закрепляются как override
|
||||
- **AND** отображаемое имя загрузки обновляется полным ярлыком
|
||||
|
||||
#### Scenario: Название источника показано ровно таким, каким закрепится
|
||||
|
||||
- **GIVEN** кандидат, название которого содержит zero-width символ или
|
||||
кириллический двойник внутри латинского слова
|
||||
- **WHEN** строится список источников для экрана ревью
|
||||
- **THEN** в строке источника и в его предпросмотре стоит очищенное название
|
||||
- **AND** выбор этого источника закрепляет то же самое значение
|
||||
|
||||
#### Scenario: Кандидат, сохранённый прежней версией, чистится на закреплении
|
||||
|
||||
- **GIVEN** кандидат, чьё название записано в хранилище без санитайзинга
|
||||
- **WHEN** человек выбирает этого кандидата
|
||||
- **THEN** закрепляется санитизированное значение, а не то, что лежит в хранилище
|
||||
|
||||
#### Scenario: Непригодное название источника пином не становится
|
||||
|
||||
- **GIVEN** кандидат, название которого не содержит ни одной буквы и ни одной
|
||||
цифры
|
||||
- **WHEN** человек выбирает этого кандидата
|
||||
- **THEN** название пином не становится, в плане остаётся название распознавания
|
||||
- **AND** провайдер, id и год закрепляются как обычно
|
||||
- **AND** отказ закрепить название виден в журнале
|
||||
Reference in New Issue
Block a user