recognize: название из метабазы санитизируется перед попаданием в план
- чистка стоит на каждой точке входа значения метабазы в план — сборка матча, копия кандидата для ревью, набор закреплённых значений источника и его чтение: гарантия, поставленная только на запись, обходится данными, сохранёнными прежними версиями - название, непригодное как имя каталога (пустое или без единой буквы и цифры), не подставляется — раздача уходит в review с названной причиной - гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям провайдера, чистится только копия, уходящая дальше
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-10
|
||||
@@ -0,0 +1,215 @@
|
||||
## Context
|
||||
|
||||
Распознавание строит план в два приёма. Сперва разбирается ответ LLM:
|
||||
`parsePlan` вызывает `sanitizePlan`, и три человекочитаемых поля — `title`,
|
||||
`original_title`, `provider_hint` — чистятся как недоверенный вход. Потом идёт
|
||||
сверка с метабазой, и при подтверждённом матче `recognize.go` подменяет поля
|
||||
плана каноническими значениями:
|
||||
|
||||
```go
|
||||
match, candidates := r.matchMetadata(ctx, plan)
|
||||
if match != nil {
|
||||
plan.Title = match.Title // ← значение внешнего сервиса, мимо чистки
|
||||
...
|
||||
}
|
||||
dec := decide(plan, pre, match, ...)
|
||||
```
|
||||
|
||||
Подстановка стоит **после** чистки, поэтому очищено ровно то, что чисткой потом
|
||||
и перезаписывается. Дальше `plan.Title` уходит в `layout.titleYear` и становится
|
||||
именем каталога библиотеки.
|
||||
|
||||
Ниже по потоку `layout.sanitizeComponent` снимает разделители пути, символы,
|
||||
недопустимые в SMB/NTFS, и байты `< 0x20`. Категорию Cf (zero-width, BOM,
|
||||
мягкий перенос) он не трогает и гомоглифы не сворачивает — они не мешают
|
||||
файловой системе и потому там не при чём.
|
||||
|
||||
Гейт авто-раскладки от этого не спасает: `metadata.go` сверяет
|
||||
`normalize(c.Title) || normalize(c.OriginalTitle)`, то есть кандидат проходит по
|
||||
**любому** из двух полей, а в план и в путь уезжает только первое.
|
||||
|
||||
Инвариант «целевой путь строго под библиотекой» при этом держится — ревью
|
||||
проверило `Dune/../../etc`, `" .. "`, `"..."` и 400 символов, выхода из
|
||||
песочницы нет. Речь о предсказуемости имени, а не о песочнице.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Название из метабазы получает ту же чистку, что и название от LLM, до того как
|
||||
попадёт в план.
|
||||
- Название, схлопнувшееся чисткой в пустое, не порождает каталог с пустым именем
|
||||
и не роняет раскладку.
|
||||
- У чистки названий остаётся один дом: второй реализации не заводится.
|
||||
- Поведение TMDB на нормальных названиях не меняется — регресс на работающем
|
||||
провайдере дороже самого дефекта.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Полная нормализация Unicode (NFC/NFKC, конфузаблы по таблице Unicode). Рамка
|
||||
задачи это прямо исключает: цель — предсказуемое и сверяемое значение, а не
|
||||
исчерпывающая защита от визуального совпадения.
|
||||
- Расширение состава `layout.sanitizeComponent`. Он чистит компонент пути под
|
||||
требования файловой системы, и категория Cf ему не мешает.
|
||||
- Чистка `director` в плане. Действующее требование `metadata-match` относит её к
|
||||
рендеру отображаемого имени, и трогать это изменение не будет.
|
||||
- Пересмотр гейта матча (`normalize(c.Title) || normalize(c.OriginalTitle)`).
|
||||
Расхождение «сверяем по двум полям, подставляем одно» после этой правки
|
||||
перестаёт быть опасным: подставляется очищенное значение.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Решение 1. Чистить на подстановке, в `recognize`
|
||||
|
||||
Санитайзинг применяется к `match.Title` в месте подстановки — там же, где
|
||||
сегодня стоит `plan.Title = match.Title`. Используется существующая
|
||||
`sanitizeTitle`, без новых функций.
|
||||
|
||||
*Почему здесь.* Место подстановки — единственная точка, где значение метабазы
|
||||
входит в план; всё, что ниже, работает уже с планом. Чистка здесь означает
|
||||
инвариант, который можно сформулировать одной фразой и проверить: **план после
|
||||
матча не содержит несанитизированных человекочитаемых полей**.
|
||||
|
||||
*Рассмотрено и отвергнуто:*
|
||||
|
||||
- **Закрыть категорию Cf и гомоглифы в `layout.sanitizeComponent`.** Дало бы
|
||||
второй дом чистке названий и разошлось бы с первым: `sanitizeTitle` сворачивает
|
||||
гомоглифы потокенно по доминирующему скрипту — правило нетривиальное, и две его
|
||||
реализации разъедутся молча. Плюс `sanitizeComponent` работает и на
|
||||
`FolderBase`, прочитанной с диска, — там свёртка гомоглифов сломала бы
|
||||
сходимость к существующей папке.
|
||||
- **Чистить в клиентах метабаз (`internal/metadata/*.go`).** Пришлось бы
|
||||
повторить в трёх клиентах и повторять в каждом следующем; проверка «в плане нет
|
||||
грязных полей» перестала бы читаться в одном месте.
|
||||
- **Прогнать `sanitizePlan` ещё раз после матча.** Внешне дешевле всего, но
|
||||
вторым проходом чистит и то, что уже чисто, а главное — молчит о случае, когда
|
||||
название базы схлопнулось в пустое: `p.Title` стал бы пустым, и раскладка
|
||||
упала бы на `layout: empty title after sanitization`.
|
||||
|
||||
### Решение 1a. Точек входа две, и закрываются обе
|
||||
|
||||
Ревью дизайна показало, что подстановка при авто-матче — не единственный вход
|
||||
значения метабазы в план. Второй: человек выбирает кандидата на ревью, его
|
||||
название закрепляется как override и попадает в план мимо распознавания. Спека
|
||||
`metadata-match` при этом сама называет ручной выбор **основным** путём
|
||||
подтверждения матча — закрыть только авто-путь значило бы починить менее
|
||||
употребимую половину и записать в спеку свойство, которого нет.
|
||||
|
||||
Закрываются обе одной и той же чисткой, но в трёх местах — по одному на каждую
|
||||
точку, где значение метабазы входит в домен:
|
||||
|
||||
1. **каноническое название матча** — в `buildMatch`, единственной точке сборки
|
||||
`Match`. Дальше по потоку значение считается чистым: и подстановка в план, и
|
||||
решение auto/review, и диагностика сухого прогона берут его как есть. Ревью
|
||||
кода показало, чем плоха чистка на подстановке: условие пригодности
|
||||
пересчитывалось независимо в двух файлах, и первая же правка одного из них
|
||||
дала бы молчаливую авто-раскладку;
|
||||
2. **названия кандидатов** — в момент, когда кандидат копируется в список для
|
||||
ревью (`matchMetadata`, накопление `candidates`). Оттуда чистое значение
|
||||
уезжает разом в хранилище, на экран ревью и в карточку Telegram;
|
||||
3. **набор пинов источника** — `sourcePins` в `worker`. Это не дубль пункта 2, а
|
||||
ответ на два разных вопроса. Во-первых, через `sourcePins` идут **и** превью
|
||||
источника, **и** его закрепление, поэтому свойство «превью = применение»
|
||||
держится конструкцией: без чистки здесь экран показывал бы одно название, а
|
||||
раскладка делала другое. Во-вторых, пункт 2 чистит **на записи**, то есть
|
||||
гарантия держалась бы на времени записи строки — все кандидаты, сохранённые
|
||||
до этой правки и стоящие в очереди ревью, обошли бы её. Чистка идемпотентна,
|
||||
поэтому на новых строках пункт 3 не делает ничего.
|
||||
|
||||
*Почему это не двигает гейт матча.* Сильный матч ищется по `cands` — срезу, как
|
||||
его отдал провайдер, — а в ревью уходит **копия** в `candidates`. Чистка на
|
||||
копировании до сравнения не доходит. Проверять это пришлось отдельно: сравнение
|
||||
идёт через `normalize`, и она **не** эквивалентна санитайзингу — zero-width
|
||||
внутри слова `normalize` превращает в пробел (`Du␀ne` → `du ne`), а санитайзинг
|
||||
удаляет (`dune`). То есть чистка всего списка `cands` до сравнения превратила бы
|
||||
часть нынешних «в review» в «авто», и это был бы сдвиг гейта, которого задача не
|
||||
заказывала.
|
||||
|
||||
*Рассмотрено и отвергнуто:* звать санитайзер прямо в `chooseCandidateLocked` —
|
||||
это закрыло бы закрепление и оставило превью считаться по сырому значению, то
|
||||
есть развело бы показанное и применённое. Правило живёт в `sourcePins`, потому
|
||||
что это единственный общий дом набора пинов; запланированный
|
||||
`review-mapping-editor` придёт туда же, а не заведёт четвёртый вызов.
|
||||
|
||||
*Что при этом экспортируется:* `recognize.SanitizeTitle` и `recognize.UsableTitle`
|
||||
— пара «почисти и проверь пригодность». Экспортировать пришлось обе: предикат без
|
||||
санитайзера обязывал бы вызывающего помнить порядок, а порядок прозой не
|
||||
проверяется.
|
||||
|
||||
### Решение 2. Пустой результат чистки — прежнее название и уход в review
|
||||
|
||||
Если `sanitizeTitle(match.Title)` даёт пустую строку, подстановка не выполняется:
|
||||
в плане остаётся название от LLM (оно уже прошло чистку и непустое — иначе
|
||||
`validateSchema` отклонил бы план), а в причины решения добавляется строка, из-за
|
||||
которой раздача уходит в ревью.
|
||||
|
||||
*Почему так.* Из трёх исходов — упасть, подставить пустое, оставить прежнее —
|
||||
только третий сохраняет работоспособность и при этом не скрывает происшествие.
|
||||
Название из одних невидимых символов означает, что с записью базы что-то не так,
|
||||
и это ровно тот случай, ради которого ревью и существует: система не уверена —
|
||||
зовёт человека, а не заминает.
|
||||
|
||||
*Рассмотрено и отвергнуто:* отклонять матч целиком (терялись бы `provider_id`,
|
||||
год и режиссёр — а они верны); подставлять пустое и ловить это раскладкой (отказ
|
||||
приходит поздно, терминальным состоянием и текстом чужого слоя — ровно та боль,
|
||||
которую чинит соседняя задача `long-title-to-review`).
|
||||
|
||||
### Решение 3. Год, режиссёр и провайдер не трогаются
|
||||
|
||||
`match.Year` — число, чистить нечего. `match.Director` в путь на диске не
|
||||
попадает, и его очистка по действующему требованию делается при рендере имени
|
||||
(`naming.sanitize`). `provider`/`provider_id` — идентификаторы, у них своя
|
||||
валидация.
|
||||
|
||||
`original_title` в план из матча **не подставляется вовсе** — сегодня
|
||||
`recognize.go` берёт из `Match` только название, год и режиссёра. Чистить там
|
||||
нечего, и требование про «поля плана после матча» это учитывает: значение
|
||||
`original_title` осталось тем, что пришло от LLM, то есть уже санитизированным.
|
||||
|
||||
### Решение 4. Решение auto/review остаётся с одним производителем
|
||||
|
||||
`Decision{Auto, Reasons}` для разобранного плана рождается ровно в одном месте —
|
||||
`decide` в `validate.go`, и её доккомментарий утверждает исчерпывающий перечень
|
||||
условий авто. Новая причина появляется **внутри** `decide`, а не дописывается к
|
||||
готовому решению со сбросом `Auto`: иначе у того же решения появляется второй
|
||||
производитель, и следующая правка гейта (соседняя задача `long-title-to-review`
|
||||
заводит причину того же класса) будет выбирать между двумя местами.
|
||||
|
||||
Дополнительного входа у `decide` при этом не появилось, хотя сперва
|
||||
предполагался: раз `Match.Title` санитизируется в `buildMatch` (Решение 1a),
|
||||
`decide` отвечает на вопрос пригодности по уже чистому значению — `match` у неё
|
||||
и так на руках. Условие считается один раз и читается из одного поля.
|
||||
|
||||
### Решение 5. Общая форма «название непригодно как компонент пути» отложена
|
||||
|
||||
Пустое-или-вырожденное название и название длиннее лимита файловой системы — один
|
||||
класс: значение не годится как компонент пути, исход один — review с названной
|
||||
причиной вместо отказа из слоя раскладки. Общее место для этого класса здесь
|
||||
**не** заводится: второй его случай — предмет соседней задачи
|
||||
`long-title-to-review`, и собирать общую форму из одного случая рано. Это
|
||||
записано, чтобы второй автор не изобретал её параллельно, а достроил.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Правка меняет работающий путь TMDB.** → Значение проходит через
|
||||
идемпотентную чистку, которая на нормальном названии не меняет ничего; критерий
|
||||
приёмки требует зелёных существующих тестов `internal/recognize` и
|
||||
`internal/metadata` **без правки ожиданий** — расхождение сразу видно.
|
||||
- **Свёртка гомоглифов может тронуть честное название.** → Правило потокенное:
|
||||
одно-скриптовый токен не трогается, и кириллические названия проходят как есть.
|
||||
Правило уже работает на выводе LLM и покрыто сценариями спеки; изменение лишь
|
||||
распространяет его на второй источник.
|
||||
- **Гомоглифы сворачиваются, а конфузаблы шире таблицы — остаются.** →
|
||||
Осознанный trade-off, названный в рамках задачи. Курируемая кирилло-латинская
|
||||
таблица закрывает реальный случай (русскоклавиатурные двойники); полная
|
||||
нормализация Unicode — отдельный разговор.
|
||||
- **Название базы схлопнулось в пустое — пользователь видит название от LLM.** →
|
||||
Раздача при этом в ревью, где название правится подсказкой, а причина названа
|
||||
словами.
|
||||
- **Уже созданные «грязные» каталоги правкой не чинятся.** → Правило сходимости
|
||||
базы папки (`file-layout`, «Сходимость базы папки при подтверждённом матче»)
|
||||
наследует имя от живой папки-якоря и не печатает его заново из распознавания.
|
||||
Каталог с невидимыми символами, созданный до этой правки, останется якорем, и
|
||||
следующий сезон ляжет в него. Лечение — переименовать папку руками, после чего
|
||||
сходимость подхватит новое имя. Изменение закрывает появление новых таких
|
||||
каталогов, а не существующие.
|
||||
@@ -0,0 +1,61 @@
|
||||
## Why
|
||||
|
||||
Название, которое система берёт из метабазы (TMDB, TVDB, TVMaze) при
|
||||
подтверждённом матче, попадает в имя каталога библиотеки Jellyfin дословно —
|
||||
таким, каким его отдал внешний сервис. Вывод LLM мы чистим и считаем
|
||||
недоверенным; название из метабазы того же обращения не получает, хотя приходит
|
||||
ровно так же — из-за периметра.
|
||||
|
||||
Итог наблюдаем: название из трёх невидимых символов (zero-width) даёт каталог,
|
||||
имя которого выглядит пустым; название с кириллической `а` внутри латинского
|
||||
слова даёт второй каталог, визуально неотличимый от первого; перевод строки
|
||||
внутри названия доезжает до плана. Ни один из этих случаев авто-раскладку не
|
||||
останавливает — она проходит, и разбирается это потом руками в библиотеке.
|
||||
|
||||
Класс не новый: тем же путём ходит TMDB с самого начала. Разговор поднят
|
||||
ревью изменения `tvdb-title-locale`, где тот же путь распространили на второго
|
||||
провайдера.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Название, взятое из метабазы при подтверждённом матче, проходит ту же чистку,
|
||||
что и название от LLM, — **до** того, как попасть в план и оттуда в путь на
|
||||
диске. Дом чистки остаётся один, второго способа чистить названия не заводится.
|
||||
- Ту же чистку проходят названия кандидатов, уходящих на ревью: выбор кандидата
|
||||
человеком — полноправный путь подтверждения матча, и закреплённое им название
|
||||
становится именем каталога так же, как название авто-матча. Условия
|
||||
подтверждения сильного матча при этом не меняются: сравнение с планом идёт по
|
||||
значению, как его отдал провайдер.
|
||||
- Название, от которого после чистки ничего не остаётся, план не перезаписывает:
|
||||
раздача уходит в ревью с названной причиной, а не раскладывается автоматически
|
||||
и не падает на пустом имени каталога.
|
||||
- Режиссёр остаётся как есть — он не участвует в пути на диске, и его чистка по
|
||||
действующему требованию делается при выводе отображаемого имени.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
Новых нет.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `metadata-match`: требование «Подтверждение матча и каноническое имя»
|
||||
дополняется — каноническое название перед подстановкой в план санитизируется,
|
||||
а название, схлопнувшееся в пустое, подстановку не выполняет и блокирует
|
||||
авто-раскладку.
|
||||
- `recognition`: требование «Санитайзинг человекочитаемых полей плана»
|
||||
перестаёт быть требованием только о выводе LLM — оно называет чистку общей для
|
||||
обоих источников названия и фиксирует, что после сверки с базой в плане не
|
||||
остаётся несанитизированных человекочитаемых полей.
|
||||
|
||||
## Impact
|
||||
|
||||
- `internal/recognize` — порядок подстановки канонического названия
|
||||
относительно санитайзинга (`recognize.go`), причина ухода в review
|
||||
(`validate.go`).
|
||||
- Поведение **обоих** провайдеров: правка меняет уже работающий путь TMDB, и это
|
||||
главный риск изменения.
|
||||
- Пути на диске не меняются ни для одной раздачи с нормальным названием:
|
||||
санитайзинг идемпотентен и на чистом значении не меняет ничего.
|
||||
- Внешних зависимостей, схемы БД и конфигурации изменение не касается.
|
||||
@@ -0,0 +1,162 @@
|
||||
# Отчёт ревью — `metadata-title-sanitize`
|
||||
|
||||
Стадия: ревью кода после apply, до archive. Отчёт триажа записан оркестратором
|
||||
(проход `review-triage` пишет только во временный каталог).
|
||||
|
||||
## Сводка
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Размер / сложность / метка | среднее / незнакомое / `large` (максимум по осям) |
|
||||
| Обоснование метки | триггер «заводится или меняется правило идентичности, слияния или разбора — построение целевых путей и санитизация имён» (`docs/review.md` → «Триггеры метки») |
|
||||
| Режим | по графу |
|
||||
| База диффа | `HEAD~1`, рабочее дерево |
|
||||
| Гейт | зелёный, прогнан триажом самостоятельно: 14 шагов `OK`, ни одного `SKIP`, `-race` реально выполнился, diff-coverage 46/46 строк (100%) |
|
||||
| Находок на входе | 16 (+1 замер): specs 4, code 3, architecture 3, adversary 3, ops 3, autotests 0 |
|
||||
| После дедупликации по причине | 11 причин + 1 найденная самим триажом |
|
||||
| Итог | 0 блокирующих, 1 «стоит исправить сейчас», 3 гипотезы, 2 promote |
|
||||
|
||||
Сигнал о заниженной метке: `review-code` возражений не подал. Второго,
|
||||
независимого от него корректора на этом прогоне не было — `basics` не
|
||||
запускался, своих тем проекта не размечено.
|
||||
|
||||
## План разметки и исход по каждой теме
|
||||
|
||||
| тема | дом | глубина | закрывает | исход |
|
||||
|---|---|---|---|---|
|
||||
| requirements | дельты change + `openspec/specs/{metadata-match,recognition}` | разбор | specs | закрыта, 4 находки (minor) |
|
||||
| autotests | `CLAUDE.md` → «Гейт» | — | autotests | закрыта, 0 находок |
|
||||
| conventions | `docs/conventions/*` | разбор | code | закрыта, 3 находки (minor), нарушений конвенций 0 |
|
||||
| architecture | `docs/architecture.md` + источник `docs/passport.md` | доказательство | architecture | закрыта, 3 находки (minor) |
|
||||
| security | `docs/security.md` | доказательство | adversary | закрыта, 2 построенных пути (major) + 1 свойство |
|
||||
| operations | `docs/architecture.md` «Эксплуатация» + источник `docs/database.md` | доказательство | ops | закрыта, 3 постмортема (2 major) + 1 замер |
|
||||
|
||||
**Тем без отчёта нет.** Тем без дома план не содержал.
|
||||
|
||||
## Что стало с каждой причиной
|
||||
|
||||
| # | причина | кто нашёл | статус |
|
||||
|---|---|---|---|
|
||||
| 1 | превью источника расходится с применением | architecture + adversary | закрыта: общий дом пинов `sourcePins`; `TestBuildSources_PreviewMatchesApply` |
|
||||
| 2 | кандидат, лежащий в БД грязным, пиннит несанитизированное название | specs + code + adversary | закрыта: чистка в `sourcePins`; `TestChooseCandidate_DirtyLegacyTitleSanitized` |
|
||||
| 3 | пин, закреплённый до деплоя, читается дословно | ops | закрыта: чистка на чтении в `applyOverrides`; `TestApplyOverrides_LegacyDirtyPinSanitized` |
|
||||
| 4 | предикат пригодности считается дважды из сырого значения | specs + code | закрыта: `decide` читает уже чистое `Match.Title` |
|
||||
| 5 | отказ закрепить название молчит | specs + code | закрыта: атрибут `title_pinned` |
|
||||
| 6 | `Match.Title` остаётся сырым, чистка в двух местах | architecture | закрыта: единственная точка сборки `buildMatch` |
|
||||
| 7 | `specs/review` обещает закрепление безусловно | specs | закрыта: заведена дельта `review` |
|
||||
| 8 | грязный каталог-якорь наследуется дальше | ops | не чинится сознательно, см. H1 |
|
||||
| 9 | название от LLM гейта пригодности не получает | adversary | вне объёма, см. P1 |
|
||||
| 10 | набор пинов пишется пятью транзакциями | ops | пре-существующее, см. H2 |
|
||||
| 11 | `resolveFolderBase` даёт `SCAN` по `recognition` | ops (замер) | пре-существующее, см. H3 |
|
||||
| 12 | режиссёр и название доезжают до карточки с RLO и zero-width | **триаж** | закрыта: `naming.sanitize` снимает Cf |
|
||||
|
||||
## Блокирует мердж
|
||||
|
||||
Пусто. Обе `major`-находки враждебного прохода и обе `major` эксплуатационного
|
||||
отработаны, каждая с падающим-без-правки тестом в дереве. Потолок не срабатывал.
|
||||
|
||||
## Стоит исправить сейчас — 1 находка (отработана)
|
||||
|
||||
### Название и режиссёр из метабазы доезжают до карточки Telegram, шапки веб-UI и имени раздачи с RLO и zero-width
|
||||
|
||||
- Файл: `internal/naming/naming.go`, `sanitize`
|
||||
- Severity: `minor` · Confidence: `high`
|
||||
- Оракул (прогон триажа): `director = "DenisVilleneuve"` →
|
||||
`label = "Dune (DenisVilleneuve, 2021)"`; `naming.sanitize` снимал только
|
||||
C0 (`r < 0x20`) и не трогал категорию Cf.
|
||||
- Последствие: дельта `recognition` этого изменения обосновывает отказ чистить
|
||||
`director` в плане тем, что «его очистка применяется при рендере отображаемого
|
||||
имени». Рендер этой гарантии для Cf не давал — то есть изменение записывало в
|
||||
спеку свойство, которого в коде нет. RLO из credits разворачивает отображение
|
||||
имени в карточке Telegram, которая для единственного пользователя и есть
|
||||
интерфейс.
|
||||
- Найдено проходом: триаж, при добыче оракула на утверждение дельты. Ни один
|
||||
проход этого не заявлял: тема `requirements` сверяла дельту с кодом изменения,
|
||||
а обоснование дельты уводило в пакет, которого дифф не касался.
|
||||
- **Отработано инлайн:** `naming.sanitize` снимает `unicode.Cf`, тест
|
||||
`TestSanitize_StripsFormatChars`.
|
||||
|
||||
## Гипотезы без доказательства
|
||||
|
||||
**H1. Грязный каталог-якорь наследует имя всем последующим раздачам того же
|
||||
матча.** Оракула нет (постмортем-рассуждение), severity снижена до `minor`.
|
||||
Предложение прохода — чистить базу, извлечённую из якоря, — дало бы вторую папку
|
||||
рядом с существующей, то есть исход, против которого затевалась задача.
|
||||
`planBase` и так гоняет `sanitizeComponent` по `FolderBase`, на уровне ФС база
|
||||
безопасна; речь только о невидимке внутри имени существующей папки. Задача не
|
||||
заводится: лечение — переименовать папку руками, записано риском в `design.md`.
|
||||
|
||||
**H2. Набор пинов пишется пятью отдельными транзакциями.** Пре-существующее,
|
||||
диффом не заведено (дифф добавил только присваивание переменной ради лог-атрибута).
|
||||
Окно — миллисекунды между пятью `UPSERT` в локальный SQLite, пути отказа никто не
|
||||
строил. Дом — цель `state-integrity`.
|
||||
|
||||
**H3. `resolveFolderBase` даёт `SCAN` по `recognition` без индекса на
|
||||
`(provider, provider_id)`.** Замер прохода `ops`: 12.9 мс при 100 загрузках и
|
||||
20000 `file_link`. Команды замера в выводе прохода нет, триаж его не
|
||||
воспроизводил — число несётся как заявленное, не как проверенное.
|
||||
Пре-существующее; дом есть — `tasks/items/scale-100-downloads.md`.
|
||||
|
||||
## Promote candidates
|
||||
|
||||
**P1. Гейт пригодности названия стоит у источников, а не на границе `layout`.**
|
||||
Путь построен триажом: `title="-"` → каталог `- (2021)`; `title="…"` → каталог
|
||||
`… (2021)`; `title="..."` → `layout: empty title after sanitization`;
|
||||
`title="/etc/passwd"` → каталог `/etc/passwd (2021)`. Инвариант «целевой путь
|
||||
строго под библиотекой» **не нарушен** (`underRoot` держит, полноширинный `/` не
|
||||
разделитель), «существующее не перезаписываем» тоже (коллизия → review). Отсюда
|
||||
`minor`. Претензия к правилу: `UsableTitle` зовут три вызывающих со стороны
|
||||
метабазы, а настоящая граница — вход в `layout`. Класс тот же, что у задачи
|
||||
`long-title-to-review`; расширить её, а не заводить новую.
|
||||
|
||||
**P2. `ident.Parse` на входных границах остаётся правилом без механизации.**
|
||||
Стоячий разрыв из `CLAUDE.md`, кандидат в `internal/archrules`.
|
||||
|
||||
## Отсев вкусовщины
|
||||
|
||||
По проектному списку «Типовые ложноположительные» не выброшено ничего — ни одна
|
||||
входящая находка под его пункты не подошла. По общим критериям выброшены две:
|
||||
«правило разложено по трём пакетам» (после переноса чистки последствия за
|
||||
пределами чтения не осталось, а отдельный дом правила у `layout` дельта
|
||||
`recognition` легитимизирует дословно) и «атрибут `title_pinned` конфлатит два
|
||||
случая» (исход в обоих один, последствия нет).
|
||||
|
||||
## Границы покрытия
|
||||
|
||||
**Запускалось:** `autotests`, `specs`, `code`, `architecture`, `adversary`, `ops`,
|
||||
`triage` — метка `large`, режим по графу. Стадия ревью дизайна прогонялась
|
||||
отдельно составом `specs`, `rubric`, `architecture`; её находки отработаны до
|
||||
реализации.
|
||||
|
||||
**Не запускалось:** `basics` — своих тем проекта не размечено, принимать нечего;
|
||||
следствие — второго, независимого от `code`, сигнала о заниженной метке не было.
|
||||
Проход независимой реализации в конвейере отсутствует (снят по стоимости),
|
||||
`idiom` упразднён 2026-08-04.
|
||||
|
||||
**Ни один из шести проходов не сообщил свой потолок и величину среза.** По
|
||||
молчанию «показал всё» неотличимо от «показал первые N» — это находка о прогоне.
|
||||
|
||||
**Правки оркестратора не видел ни один проход.** Все шесть отчётов сняты с
|
||||
состояния кода до исправлений; семь закрытых причин — правки, по которым ревью не
|
||||
проводилось. Их проверка сводится к зелёному гейту со 100% diff-coverage, трём
|
||||
новым падающим-без-правки тестам и чтению кода в точках единственности
|
||||
(`buildMatch`, `sourcePins`, `applyOverrides`). Находка №12 — прямое следствие:
|
||||
она живёт в пакете, которого дифф не касался.
|
||||
|
||||
**Не проверит ни один проход** (`docs/review.md` → «Недоступно проверке»):
|
||||
история инцидентов на umbar; поведение SQLite под реальным объёмом и профилем;
|
||||
завязка внешних потребителей (Jellyfin, закладки) на текущее поведение; суждение
|
||||
«этой функциональности не должно существовать»; качество распознавания как
|
||||
такового.
|
||||
|
||||
**Перестали проверять сознательно:** идиоматичность Go — с 2026-08-04, различение
|
||||
«идиоматично против распространено» не спрашивает никто; класс обратимый,
|
||||
пересмотр — задача `quality-review-agents`.
|
||||
|
||||
**Решения и наблюдения проекта прогон не читал:** `docs/adr/` и `docs/research/` —
|
||||
процессные документы. Расхождение изменения с записанным решением ловит сверка
|
||||
документации (`av-dev-docs:healthcheck`), а не ревью.
|
||||
|
||||
**Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.**
|
||||
На этом изменении — правило разбора и санитизации — именно такой проход на прошлом
|
||||
прогоне приносил независимый оракул.
|
||||
+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** отказ закрепить название виден в журнале
|
||||
@@ -0,0 +1,90 @@
|
||||
## 1. Код
|
||||
|
||||
- [x] 1.1 В `internal/recognize/recognize.go` прогнать `match.Title` через
|
||||
`sanitizeTitle` перед подстановкой в `plan.Title`; год, режиссёр и провайдер
|
||||
оставить как есть
|
||||
- [x] 1.2 Непригодный результат чистки (пусто либо ни одной буквы и цифры):
|
||||
подстановку не выполнять, прежнее название сохранить, причину передать **в**
|
||||
`decide`, а не дописывать к готовому решению (см. design, Решение 4)
|
||||
- [x] 1.3 Логировать на `Debug`, когда чистка реально изменила каноническое
|
||||
название — тем же способом, что `sanitizeField` (названия не секреты)
|
||||
- [x] 1.4 Чистить `Title`/`OriginalTitle` кандидата на **копии**, в момент
|
||||
`append` в `candidates` (`internal/recognize/metadata.go`). Место накопления не
|
||||
двигать — оно стоит до `if match != nil { continue }`, и перенос обеднил бы
|
||||
список кандидатов при подтверждённом матче с заблокированным авто. Срез `cands`
|
||||
не мутировать: `for _, c := range cands` даёт копию структуры (все поля
|
||||
`metadata.Candidate` скалярные), поэтому сравнение остаётся на значениях
|
||||
провайдера и гейт матча не сдвигается
|
||||
- [x] 1.5 Закрепление кандидата с непригодным названием (`chooseCandidateLocked`,
|
||||
`internal/worker/review.go`): пин названия не ставится, в плане остаётся
|
||||
название распознавания — тот же исход, что у канонического названия
|
||||
|
||||
## 2. Тесты
|
||||
|
||||
- [x] 2.1 Табличный тест на четыре входа из «Воспроизведения» задачи: три ZWSP
|
||||
(`U+200B`), `Dune<RLO>gnp.mkv` (`U+202E`), `Dune\nHACK`, `Dunа` с кириллической
|
||||
`а` — проверяет `plan.Title` и `Decision.Auto`
|
||||
- [x] 2.2 Граничные случаи непригодного названия: три ZWSP (пусто после чистки) и
|
||||
вырожденные `"."`, `"..."`, `"-"`, `" . "` — план сохраняет прежнее название,
|
||||
`Auto=false`, причина названа, год и провайдер подставлены
|
||||
- [x] 2.5 Идемпотентность: `sanitizeTitle(sanitizeTitle(x)) == sanitizeTitle(x)`
|
||||
по тому же набору входов, что и 2.1
|
||||
- [x] 2.6 Кандидаты, уходящие в ревью, очищены: те же четыре входа в названии
|
||||
кандидата — в `Result.Candidates` значения санитизированы
|
||||
- [x] 2.7 Гейт матча не сдвинулся: кандидат, отличающийся от плана только
|
||||
zero-width внутри слова, по-прежнему **не** даёт подтверждённого матча
|
||||
(сравнение идёт по значению провайдера)
|
||||
- [x] 2.8 Список кандидатов не обеднел: при подтверждённом матче от первого
|
||||
провайдера кандидаты остальных провайдеров того же ключа по-прежнему в списке
|
||||
- [x] 2.9 Закрепление кандидата с названием `-`: пин названия не поставлен, в
|
||||
плане остаётся название распознавания, каталог с мусорным именем не строится
|
||||
- [x] 2.3 Нормальное название матча: значение в плане совпадает с ответом базы,
|
||||
`Auto=true` при прочих чистых условиях
|
||||
- [x] 2.4 Прогнать существующие тесты `internal/recognize` и `internal/metadata`
|
||||
**без правки ожиданий** — регресс на TMDB виден сразу
|
||||
|
||||
## 3. Гейт и спеки
|
||||
|
||||
- [x] 3.1 `task gate` зелёный
|
||||
- [x] 3.2 `openspec validate --strict metadata-title-sanitize`
|
||||
|
||||
## Критерии приёмки задачи
|
||||
|
||||
Приходят из постановки `tasks/items/metadata-title-sanitize.md`, здесь — дословно.
|
||||
|
||||
- [x] A1 Все четыре входа из «Воспроизведения» дают либо санитизированный
|
||||
`plan.Title`, либо уход в review — но не авто-раскладку с исходным значением.
|
||||
**Оракул:** тот самый падающий тест из отчёта триажа, перенесённый в дерево
|
||||
- [x] A2 Название, схлопывающееся санитизацией в пустую строку, не порождает
|
||||
каталог с пустым именем и не роняет раскладку. **Оракул:** табличный тест на
|
||||
границе, случай «три ZWSP»
|
||||
- [x] A3 Поведение TMDB на нормальных названиях не изменилось. **Оракул:**
|
||||
существующие тесты `internal/recognize` и `internal/metadata` зелёные без правок
|
||||
ожиданий
|
||||
- [x] A4 Место санитизации названо требованием спеки, а не только кодом.
|
||||
**Оракул:** `openspec validate --strict` на дельте
|
||||
|
||||
## 4. Правки по ревью кода
|
||||
|
||||
- [x] 4.1 Санитайзинг канонического названия перенесён в `buildMatch` —
|
||||
единственную точку сборки `Match`; `recognize.go` и `decide` читают уже чистое
|
||||
значение и не пересчитывают условие каждый по-своему
|
||||
- [x] 4.2 Чистка и проверка пригодности названия источника перенесены в
|
||||
`sourcePins` — общий дом набора пинов, через который идут и превью, и
|
||||
закрепление; «превью = применение» держится конструкцией
|
||||
- [x] 4.3 Кандидаты, сохранённые прежней версией, чистятся на закреплении:
|
||||
гарантия не держится на времени записи строки
|
||||
- [x] 4.4 Отказ закрепить непригодное название виден в журнале — атрибут
|
||||
`title_pinned` у существующей записи `review candidate chosen`
|
||||
- [x] 4.5 Дельта `review` — требование «Подтверждение матча обновляет
|
||||
отображаемое имя» дополнено санитайзингом на закреплении и свойством
|
||||
«превью = применение»
|
||||
- [x] 4.6 Тесты: `TestChooseCandidate_DirtyLegacyTitleSanitized`,
|
||||
`TestBuildSources_PreviewMatchesApply` (оба падают без правки)
|
||||
- [x] 4.7 Чистка названия и на **чтении** пина (`applyOverrides`): пин,
|
||||
закреплённый прежней версией, обычным «Применить» не переписывается, и без
|
||||
этого доезжал до имени каталога дословно (находка эксплуатационного прохода)
|
||||
- [x] 4.8 `naming.sanitize` снимает категорию Cf: дельта `recognition`
|
||||
обосновывает отказ чистить `director` в плане тем, что его чистит рендер, — а
|
||||
рендер снимал только C0, и RLO из credits разворачивал карточку Telegram
|
||||
(находка триажа)
|
||||
@@ -54,6 +54,26 @@
|
||||
кандидаты уходят в review). Работа с базами опциональна: при выключенных базах
|
||||
сверка не выполняется и подтверждённого матча нет.
|
||||
|
||||
Каноническое название — **недоверенное значение внешнего сервиса**, и перед
|
||||
подстановкой в план система SHALL применять к нему тот же санитайзинг
|
||||
человекочитаемых полей, что и к выводу LLM (см. `recognition`, требование
|
||||
«Санитайзинг человекочитаемых полей плана»). Подстановка несанитизированного
|
||||
значения SHALL NOT выполняться: план — источник имени каталога библиотеки, и
|
||||
значение, не прошедшее чистку, уходит в путь на диске.
|
||||
|
||||
Если после санитайзинга каноническое название оказывается **непригодным как
|
||||
компонент пути** — пустым либо вырожденным, то есть не содержащим ни одной буквы
|
||||
и ни одной цифры (`.`, `..`, `-`, только пунктуация), — система SHALL сохранить в
|
||||
плане прежнее название и SHALL NOT разрешать авто-раскладку: причина уходит в
|
||||
перечень причин решения, раздача попадает в review. Проверка пригодности SHALL
|
||||
стоять **после** санитайзинга, а не до него. Ронять распознавание или раскладку
|
||||
такой матч SHALL NOT — год, провайдер и `provider_id` при этом подставляются как
|
||||
обычно.
|
||||
|
||||
Санитайзинг, **изменивший** каноническое название, но оставивший его пригодным,
|
||||
авто-раскладку SHALL NOT блокировать: в план идёт очищенное значение, и оно
|
||||
предсказуемо — ради этого чистка и стоит.
|
||||
|
||||
При подтверждённом матче система SHALL дополнительно попытаться получить из базы
|
||||
**режиссёра** (TMDB/TVDB credits) и вложить его в план (`director`) как
|
||||
недоверенное косметическое значение для вывода отображаемого имени. Тот же способ
|
||||
@@ -77,6 +97,36 @@ plan-санитайзинге.
|
||||
- **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
|
||||
@@ -386,3 +436,51 @@ fallback: если первый проход подтвердил матч ил
|
||||
- **THEN** гейт даёт двух сильных кандидатов вместо одного
|
||||
- **AND** подтверждённого матча нет, обе записи уходят кандидатами в review
|
||||
|
||||
### 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** исход подтверждения матча тот же, что был до этого изменения
|
||||
|
||||
|
||||
@@ -179,6 +179,25 @@ per-file `season`/`episode` (отдельного скалярного `season`
|
||||
последовательности пробельных к одиночному пробелу и обрезать края; (3) сворачивать
|
||||
кирилло-латинские homoglyph-двойники (см. ниже).
|
||||
|
||||
Санитайзинг SHALL быть **общим для обоих источников названия** — вывода LLM и
|
||||
записи метабазы. Значение, пришедшее из метабазы и заменяющее поле плана при
|
||||
подтверждённом матче, SHALL проходить тот же санитайзинг перед подстановкой (см.
|
||||
`metadata-match`, требование «Подтверждение матча и каноническое имя»). Второго
|
||||
способа чистить **названия плана** система SHALL NOT заводить: одна реализация
|
||||
обслуживает оба источника. Речь только о полях плана — очистка компонента пути
|
||||
под требования файловой системы (`layout`) и очистка отображаемого ярлыка
|
||||
(`naming`) остаются своими, отдельными и законными.
|
||||
|
||||
Санитайзинг SHALL быть **идемпотентным**: повторное применение к уже
|
||||
санитизированному значению SHALL NOT изменять его. На этом свойстве стоит
|
||||
утверждение, что правка не меняет путей на диске для раздач с нормальным
|
||||
названием, и оно проверяется тестом, а не подразумевается.
|
||||
|
||||
Следствие, на которое опирается раскладка: к моменту, когда план уходит в решение
|
||||
auto/review, каждое из полей `title`, `original_title`, `provider_hint` совпадает
|
||||
с тем, что дал бы санитайзинг этого значения — независимо от того, пришло оно от
|
||||
LLM или из метабазы.
|
||||
|
||||
Свёртка двойников SHALL работать потокенно (по словам, разделённым не-буквенными
|
||||
символами): токен, все буквы которого принадлежат одному скрипту, система SHALL
|
||||
оставлять без изменений (билингвальность реальна — кириллические названия
|
||||
@@ -187,10 +206,15 @@ per-file `season`/`episode` (отдельного скалярного `season`
|
||||
двойниками из доминирующего скрипта по курируемой таблице. При отсутствии
|
||||
доминирующего скрипта (равенство) токен SHALL оставаться без изменений.
|
||||
|
||||
Санитайзинг SHALL применяться ТОЛЬКО к перечисленным человекочитаемым полям.
|
||||
Санитайзинг SHALL применяться ТОЛЬКО к перечисленным человекочитаемым полям
|
||||
**плана**. Применение того же санитайзинга к значениям, пришедшим из метабазы —
|
||||
каноническому названию и названиям кандидатов, — заказано отдельно, см.
|
||||
`metadata-match`.
|
||||
`files[].src` система SHALL NOT санитизировать — эти значения обязаны совпадать с
|
||||
реальными файлами торрента, и расхождение (в т.ч. homoglyph) SHALL оставаться
|
||||
основанием отклонить план, а не поводом «чинить» путь.
|
||||
основанием отклонить план, а не поводом «чинить» путь. `director` система SHALL
|
||||
NOT санитизировать в плане — он не участвует в пути на диске, и его очистка
|
||||
применяется при рендере отображаемого имени (см. `metadata-match`).
|
||||
|
||||
Когда санитайзинг реально изменил значение поля, система SHALL логировать это на
|
||||
уровне `Debug` (названия не относятся к секретам).
|
||||
@@ -233,3 +257,26 @@ per-file `season`/`episode` (отдельного скалярного `season`
|
||||
- **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** значение не изменяется
|
||||
|
||||
|
||||
@@ -231,6 +231,20 @@ LLM; нет матча в базе или несколько кандидато
|
||||
считаться как выбор кандидата из списка совпадений, так и ручное добавление
|
||||
источника по id/URL (оба закрепляют провайдера и каноническое название).
|
||||
|
||||
Закрепляемое название источника SHALL проходить санитайзинг человекочитаемых
|
||||
полей (см. `recognition`) и проверку пригодности как компонента пути (см.
|
||||
`metadata-match`) — на **общей** точке сборки набора пинов источника, той же,
|
||||
через которую строится предпросмотр. Отсюда следует свойство, на которое
|
||||
опирается экран ревью: показанное для источника название и путь совпадают с тем,
|
||||
что закрепится и разложится по выбору этого источника. Название, непригодное как
|
||||
имя каталога, пином SHALL NOT становиться — в плане остаётся название
|
||||
распознавания, а факт отказа SHALL быть наблюдаем в журнале.
|
||||
|
||||
Санитайзинг на закреплении SHALL применяться независимо от того, было ли значение
|
||||
очищено при сохранении кандидата: гарантия чистоты не может держаться на времени
|
||||
записи строки, иначе кандидаты, сохранённые прежними версиями, обходят её. Тот же
|
||||
санитайзинг идемпотентен, поэтому на уже очищенном значении он ничего не меняет.
|
||||
|
||||
При закреплении выбранного/добавленного источника система SHALL best-effort
|
||||
получить режиссёра этого источника из метабазы (credits по `provider:id`, см.
|
||||
`metadata-match`) и закрепить его как override, чтобы он попал в эффективные поля
|
||||
@@ -244,46 +258,35 @@ SHALL быть best-effort по отношению к qBittorrent: недост
|
||||
NOT проваливать команду ревью. Это согласуется с инвариантом «авто-действие
|
||||
только при подтверждённом матче».
|
||||
|
||||
#### Scenario: Выбор кандидата обновляет имя
|
||||
#### Scenario: Выбор кандидата переливает каноническое имя
|
||||
|
||||
- **GIVEN** загрузка в ревью с пустым или неинформативным `display_name`
|
||||
(например, «Unknown») и списком кандидатов
|
||||
- **WHEN** пользователь выбирает кандидата, подтверждая матч
|
||||
- **THEN** выбор кандидата закрепляется как и прежде
|
||||
- **AND** `download.display_name` обновляется полным ярлыком
|
||||
«Название (режиссёр, год)» (для сериала — со сводкой сезонов)
|
||||
- **AND** раздача в qBittorrent переименовывается в то же имя
|
||||
- **GIVEN** загрузка в ревью с кандидатами метабазы
|
||||
- **WHEN** человек выбирает кандидата
|
||||
- **THEN** провайдер, id и каноническое название закрепляются как override
|
||||
- **AND** отображаемое имя загрузки обновляется полным ярлыком
|
||||
|
||||
#### Scenario: Ручное добавление источника обновляет имя
|
||||
#### Scenario: Название источника показано ровно таким, каким закрепится
|
||||
|
||||
- **GIVEN** загрузка в ревью без совпадений в списке
|
||||
- **WHEN** пользователь вручную добавляет источник по id/URL, подтверждая матч
|
||||
- **THEN** источник закрепляется как и прежде
|
||||
- **AND** `download.display_name` и имя раздачи в qBittorrent обновляются
|
||||
полным ярлыком подтверждённого источника
|
||||
- **GIVEN** кандидат, название которого содержит zero-width символ или
|
||||
кириллический двойник внутри латинского слова
|
||||
- **WHEN** строится список источников для экрана ревью
|
||||
- **THEN** в строке источника и в его предпросмотре стоит очищенное название
|
||||
- **AND** выбор этого источника закрепляет то же самое значение
|
||||
|
||||
#### Scenario: Выбор кандидата подтягивает режиссёра в ярлык
|
||||
#### Scenario: Кандидат, сохранённый прежней версией, чистится на закреплении
|
||||
|
||||
- **GIVEN** загрузка в ревью, у выбранного кандидата в credits метабазы указан
|
||||
режиссёр
|
||||
- **WHEN** пользователь выбирает кандидата, подтверждая матч
|
||||
- **THEN** режиссёр best-effort извлекается из метабазы и закрепляется override
|
||||
- **AND** `download.display_name` получает полный ярлык с этим режиссёром
|
||||
- **GIVEN** кандидат, чьё название записано в хранилище без санитайзинга
|
||||
- **WHEN** человек выбирает этого кандидата
|
||||
- **THEN** закрепляется санитизированное значение, а не то, что лежит в хранилище
|
||||
|
||||
#### Scenario: Режиссёр кандидата недоступен — выбор не ломается
|
||||
#### Scenario: Непригодное название источника пином не становится
|
||||
|
||||
- **GIVEN** выбор кандидата, для которого credits недоступны или режиссёра нет
|
||||
- **WHEN** пользователь подтверждает матч
|
||||
- **THEN** выбор источника выполнен, режиссёр берётся из сохранённого контекста
|
||||
или остаётся пустым
|
||||
- **AND** команда ревью не возвращает ошибку
|
||||
|
||||
#### Scenario: Недоступность qBittorrent не ломает выбор кандидата
|
||||
|
||||
- **GIVEN** выбор кандидата в ревью
|
||||
- **WHEN** переименование раздачи в qBittorrent завершается ошибкой
|
||||
- **THEN** выбор кандидата и обновление `download.display_name` выполнены
|
||||
- **AND** команда ревью не возвращает ошибку
|
||||
- **GIVEN** кандидат, название которого не содержит ни одной буквы и ни одной
|
||||
цифры
|
||||
- **WHEN** человек выбирает этого кандидата
|
||||
- **THEN** название пином не становится, в плане остаётся название распознавания
|
||||
- **AND** провайдер, id и год закрепляются как обычно
|
||||
- **AND** отказ закрепить название виден в журнале
|
||||
|
||||
### Requirement: Инфо и предпросмотр выбранного источника
|
||||
|
||||
|
||||
Reference in New Issue
Block a user