Files
avandClaude Opus 4.8 e2ea1840c9 Распознавание: санитайзинг названий от LLM + безгодовой фолбэк сверки
Кейс «Harold and the Purple Crayon»: LLM отдал title с кириллической
буквой-двойником, сырое название ушло в запрос TVDB дословно (не нашлось),
а гейт нормализации кир/лат двойники не сворачивал — двойной промах, пустой
список кандидатов, ручной ввод id.

- recognition: санитайзинг человекочитаемых полей плана (title/original_title/
  provider_hint) на границе разбора, до валидации: strip control/zero-width,
  collapse пробелов, потокенная свёртка homoglyph-двойников по курируемой
  кир↔лат таблице. files[].src не трогаем (обязаны биться с торрентом).
- metadata-match: тот же fold в normalize (гейт) как defense-in-depth;
  безгодовой второй проход сверки как fallback при известном годе и промахе
  первого — восстанавливает off-by-one авто-матчи и пополняет кандидатов
  review. В fallback требуем известный год кандидата (год-unknown → review,
  не авто); гейт год ±1 и инвариант авто-матча не двигаются.

Спеки recognition/metadata-match обновлены, change заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 15:45:19 +03:00

132 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## MODIFIED Requirements
### Requirement: Сверка с базой по нескольким названиям
При сверке плана с включёнными базами метаданных система SHALL искать по
нескольким названиям в порядке убывания силы ключа: сначала по
`original_title`, затем по локализованному `title`, затем по `provider_hint`.
Этот перебор названий SHALL выполняться в рамках каждого прохода сверки — проходы
(с годом и безгодовой fallback) определяет требование «Безгодовой второй проход
сверки как fallback». Поиск SHALL останавливаться, как только очередной запрос
дал единичный сильный матч (ровно один кандидат с совпадением названия и года);
ранняя остановка действует в пределах прохода. Запрос с названием, нормализованно
совпадающим с уже выполненным, система SHALL пропускать, чтобы не обращаться к
базе повторно с тем же ключом.
Кандидаты для ручного выбора в review система SHALL собирать из всех выполненных
заходов обоих проходов с дедупликацией по `provider:id` и общим потолком.
#### Scenario: Иностранный фильм находится по оригинальному названию
- **GIVEN** план с `title` «Тёмный рыцарь», `original_title` «The Dark Knight», год 2008
- **WHEN** выполняется сверка с базой
- **THEN** первый запрос идёт по «The Dark Knight»
- **AND** при единичном сильном матче дальнейшие запросы (по `title`, `provider_hint`) не выполняются
#### Scenario: Фолбэк на локализованное название
- **GIVEN** план, для которого запрос по `original_title` не дал единичного сильного матча
- **WHEN** продолжается сверка
- **THEN** выполняется запрос по локализованному `title`
- **AND** при отсутствии матча и там — запрос по `provider_hint`
#### Scenario: Дублирующий запрос пропускается
- **GIVEN** план, у которого `original_title` нормализованно совпадает с `title`
- **WHEN** выполняется сверка
- **THEN** база запрашивается этим названием один раз, повторный заход по `title` не делается
### Requirement: Нормализация названий при сравнении
Нормализация названий для гейта сильного матча SHALL сводить букву `ё` к `е`,
чтобы написания, различающиеся только `ё`/`е`, считались одним названием.
Нормализация SHALL дополнительно сворачивать кирилло-латинские homoglyph-двойники
по той же курируемой таблице, что применяется при санитайзинге плана
(capability `recognition`), чтобы визуально совпадающие название плана и кандидата
базы, различающиеся лишь скриптом отдельных букв, считались одним названием. Это
defense-in-depth на случай двойников со стороны кандидата: гейт — точка
безопасно-критичного решения об авто-матче и SHALL быть устойчив к двойникам
независимо от предшествующего санитайзинга.
#### Scenario: «Тёмный» и «Темный» совпадают
- **GIVEN** план с названием «Тёмный рыцарь» и кандидат базы «Темный рыцарь»
- **WHEN** сравниваются нормализованные названия
- **THEN** они считаются совпадающими
#### Scenario: Двойник у кандидата не мешает матчу
- **GIVEN** план с латинским названием `Harold` и кандидат базы, где то же слово
содержит кириллический символ-двойник
- **WHEN** сравниваются нормализованные названия
- **THEN** они считаются совпадающими
## ADDED Requirements
### Requirement: Безгодовой второй проход сверки как fallback
Система SHALL выполнять второй проход сверки без года как fallback: когда поиск с
годом не дал подтверждённого единичного сильного матча, а год плана известен
(`year > 0`), выполняется тот же перебор названий (в порядке `original_title`
`title``provider_hint`) и провайдеров, но с запросом БЕЗ года. Второй проход
SHALL выполняться только как
fallback: если первый проход подтвердил матч или год плана неизвестен, второго
прохода быть SHALL NOT.
Гейт сильного матча (нормализованное совпадение названия и год кандидата в пределах
±1, ровно один кандидат) при этом SHALL оставаться неизменным — безгодовой проход
расширяет только выдачу запроса, но не ослабляет условие подтверждения, поэтому
инвариант «авто-раскладка только при подтверждённом единичном сильном матче» не
затрагивается. Дополнительно в безгодовом проходе система SHALL требовать
известный год кандидата: запись с неизвестным годом (год подтвердить нечем) во
втором проходе подтверждённым матчем быть SHALL NOT и уходит кандидатом в review
(в первом, с-годом, проходе прежняя leniency к неизвестному году сохраняется).
Кандидаты для ручного выбора в review система SHALL собирать из обоих проходов с
той же дедупликацией по `provider:id` и общим потолком.
#### Scenario: Год плана off-by-one — авто-матч восстанавливается без года
- **GIVEN** запись есть в базе, но год плана отличается от её года ровно на 1
(жёсткий exact-year фильтр запроса отсёк её в первом проходе)
- **WHEN** первый проход (с годом) не дал сильного матча
- **THEN** выполняется второй проход без года
- **AND** единичный кандидат проходит гейт (название совпадает, год бьётся ±1) —
матч подтверждается
#### Scenario: Год плана расходится больше чем на 1 — кандидат только в review
- **GIVEN** запись есть в базе, но год плана отличается от её года больше чем на 1
- **WHEN** второй проход без года возвращает эту запись единственным кандидатом
- **THEN** гейт отклоняет её по году (расхождение больше ±1) — подтверждённого
матча нет
- **AND** кандидат собирается для выбора в review (человек получает кандидата
вместо пустого списка)
#### Scenario: Первый проход подтвердил матч — второго нет
- **GIVEN** первый проход (с годом) дал единичный сильный матч
- **WHEN** завершается сверка
- **THEN** второй (безгодовой) проход не выполняется
#### Scenario: Год неизвестен — второго прохода нет
- **GIVEN** план без года (`year` = 0)
- **WHEN** первый проход не дал матча
- **THEN** второй проход не выполняется (он был бы идентичен первому)
#### Scenario: Кандидат с неизвестным годом в безгодовом проходе — только review
- **GIVEN** год плана известен, а pass 1 (с годом) не дал матча
- **WHEN** безгодовой проход возвращает единственного кандидата с совпадающим
названием, но неизвестным годом
- **THEN** подтверждённого матча нет (год кандидата не подтверждён) — авто-раскладка
не делается
- **AND** кандидат собирается для выбора в review
#### Scenario: Несколько кандидатов из безгодового прохода — матч не подтверждён
- **GIVEN** безгодовой проход вернул более одного кандидата, проходящего гейт
- **WHEN** оценивается матч
- **THEN** подтверждённого матча нет, кандидаты собираются для выбора в review