# ingest Specification

## ADDED Requirements

### Requirement: Сохранение извлечённой из контекста структуры имени

Система SHALL сохранять структуру имени, извлечённую LLM со структурированным
выводом на шаге добавления (тип, название, оригинальное название, год, режиссёр,
сезон), у загрузки (`download.parsed_context`, JSON), чтобы её поля могли
переиспользоваться при последующем выводе имени без повторного вызова LLM.
Алгоритмический фолбек структуры не даёт (его выход — только строка имени), тогда
`parsed_context` остаётся пустым — это штатно (нижний слой отсутствует). Сохранённая структура SHALL
быть **базовым (наименее доверенным) слоем** источника полей имени: её значения
берутся, только если более доверенный слой (распознавание/матч, ручные правки)
соответствующего поля не дал.

Сохранение SHALL быть best-effort и косметическим: неудача записи `parsed_context`
SHALL NOT проваливать добавление загрузки, а сама структура SHALL влиять только на
отображаемое имя и SHALL NOT влиять на пути файлов, распознавание или раскладку.
Пустая/невыведенная структура (нет контекста и подсказки) SHALL приводить к
пустому `parsed_context` (нечего сохранять).

#### Scenario: Извлечённый режиссёр сохраняется у загрузки

- **GIVEN** контекст загрузки, из которого LLM извлёк режиссёра и год
- **WHEN** система выводит имя на шаге добавления
- **THEN** извлечённая структура (в т.ч. режиссёр) сохраняется в
  `download.parsed_context`
- **AND** отображаемое имя формируется как и прежде (полный ярлык)

#### Scenario: Сбой сохранения структуры не валит добавление

- **GIVEN** запись `parsed_context` завершается ошибкой
- **WHEN** идёт шаг добавления загрузки
- **THEN** загрузка всё равно добавляется в qBittorrent (с `rename`, если имя
  выведено)
- **AND** ошибка логируется, приём/добавление не проваливается

#### Scenario: Пустой вход — пустая структура

- **GIVEN** пойманная загрузка без контекста и без подсказки из полей источника
- **WHEN** выполняется шаг добавления
- **THEN** структура не выводится, `download.parsed_context` пуст

## MODIFIED Requirements

### Requirement: Обновление отображаемого имени по распознаванию

Система SHALL уметь обновлять отображаемое имя загрузки после того, как
распознавание дало каноническое название, — переливая уже вычисленное имя (без
нового вызова LLM) в `download.display_name` и в имя раздачи qBittorrent.

Источником имени SHALL быть **эффективные поля имени**, разрешённые по слоям
сверху вниз (берётся первый непустой слой): (1) ручные правки `override`;
(2) распознавание с вложенным подтверждённым матчем (`recognition`, куда матч
метабазы уже вложил каноничные название/год/режиссёра); (3) сохранённая
структура из контекста (`download.parsed_context`). Так каждое поле берётся из
самого доверенного доступного источника, а данные из контекста (например
режиссёр) не теряются, если распознавание/матч их не дали. Название из слоя
распознавания SHALL совпадать с тем, что использует раскладка (эффективный
`title`), чтобы отображаемое имя не расходилось с целевыми путями.

Формат SHALL быть тем же полным детерминированным ярлыком, что и на шаге
добавления: «Название (режиссёр, год)», где режиссёр и год опциональны, а для
сериала добавляется сводка сезонов («. Сезон N» для одного сезона; «. Сезоны …»
для многосезонного пака; отметка спецвыпусков) — согласованная со сводкой сезонов
на экране просмотра. Применяются та же очистка от управляющих символов и обрезка
по ограничению длины. Пустой источник (нет ни распознавания, ни сохранённой
структуры, дающих непустое название) SHALL приводить к отсутствию изменений
(no-op).

Переименование раздачи в qBittorrent SHALL адресоваться по infohash своей
раздачи и SHALL быть best-effort: сбой (раздача удалена, qBittorrent недоступен)
SHALL NOT проваливать обновление — `download.display_name` обновляется в любом
случае, ошибка внешнего вызова логируется. Как и на шаге добавления,
отображаемое имя SHALL влиять только на отображение и SHALL NOT влиять на пути
файлов, распознавание или раскладку.

Обновление имени SHALL иметь две точки входа: **авто** — при подтверждённом
матче (см. capability `review`); **ручную** — по явному действию пользователя.
Ручное действие SHALL перезаписывать текущее имя всегда; авто SHALL перезаписывать,
когда выведенное имя непусто.

#### Scenario: Перелив имени в загрузку и раздачу

- **GIVEN** загрузка с распознанным непустым каноническим названием и известным
  режиссёром (из матча или из сохранённого контекста)
- **WHEN** запускается обновление отображаемого имени
- **THEN** `download.display_name` устанавливается в полный ярлык
  «Название (режиссёр, год)» (для сериала — со сводкой сезонов)
- **AND** раздача в qBittorrent переименовывается в то же имя (по infohash своей
  раздачи)

#### Scenario: Режиссёр из контекста переживает распознавание без матча

- **GIVEN** загрузка, где режиссёр был извлечён из контекста, а распознавание
  прошло без подтверждённого матча (режиссёр из метабазы недоступен)
- **WHEN** запускается обновление отображаемого имени
- **THEN** в ярлыке используется режиссёр из сохранённого контекста
- **AND** название/год берутся из распознавания

#### Scenario: Режиссёр из матча бьёт контекстного

- **GIVEN** загрузка, где режиссёр есть и в контексте, и в подтверждённом матче
- **WHEN** формируется ярлык
- **THEN** используется режиссёр из матча (более доверенный слой)

#### Scenario: qBittorrent недоступен — имя у загрузки всё равно обновлено

- **GIVEN** обновление отображаемого имени с выведенным непустым именем
- **WHEN** переименование раздачи в qBittorrent завершается ошибкой (недоступен
  или раздача удалена)
- **THEN** `download.display_name` всё равно обновлён
- **AND** ошибка внешнего вызова qBittorrent логируется, операция не проваливается

#### Scenario: Нет источника имени — обновление ничего не делает

- **GIVEN** загрузка без распознанного названия и без сохранённой структуры
  (пустой источник имени)
- **WHEN** запускается обновление отображаемого имени
- **THEN** ни `download.display_name`, ни имя раздачи не меняются (no-op)
