## MODIFIED Requirements ### Requirement: Разбор сигналов LLM в структурированный план Система SHALL передавать LLM недоверенные сигналы (имя торрента, дерево файлов с размерами, текстовый контекст и накопленные подсказки, пред-парс) и получать структурированный план в схеме: `type` (`movie`|`series`), `title`, `original_title`, `year`, `provider_hint`, `files[]` и `confidence`. Каждый элемент `files[]` SHALL адресовать файл раздачи номером строки из напечатанного системой списка файлов, а не копией пути, и нести `role` (`main`|`episode`|`subtitle`|`extra`|`sample`|`ignore`) и, для сериала, per-file `season`/`episode` (отдельного скалярного `season` быть SHALL NOT — так выражаются мультисезонные паки и спецвыпуски). Путь файла (`src`) SHALL подставлять сама система резолвом номера по своему списку. Ответ, где элемент `files[]` несёт путь вместо номера, система SHALL принимать как запасной формат, проверяя совпадение пути с реальным файлом торрента. Элемент, несущий и номер, и путь, система SHALL резолвить по номеру; несовпадение присланного пути с резолвом номера SHALL давать причину ухода в `review` (сигнал сдвига адресации), а не молчаливое затирание. Негодный элемент `files[]` — номер вне диапазона списка, повторная адресация уже адресованного файла, отсутствие и номера, и пути — система SHALL отбрасывать с причиной ухода в `review`, сохраняя остальной план; при повторной адресации в плане SHALL оставаться первый элемент. Отбраковка отдельных элементов SHALL NOT считаться ошибкой разбора и SHALL NOT порождать повторный запрос к модели. План, в котором после отбраковки не осталось ни одного файла, система SHALL считать неразобранным. Порядок списка файлов SHALL быть свойством самого распознавания, а не дисциплиной вызывающего: распознавание SHALL приводить полученный список к детерминированному порядку само, печатать промпт и резолвить номера по одному и тому же упорядоченному списку. Повторное распознавание той же раздачи SHALL давать ту же нумерацию. План MAY дополнительно нести опциональное скалярное поле `director` (режиссёр). Это поле НЕ требуется от LLM и НЕ участвует в структурной валидации или гейте авто-раскладки; его заполняют подтверждённый матч метабазы (авто) или закреплённый в ревью выбранный источник (через override, см. `metadata-match`/`review`) как недоверенное косметическое значение для вывода отображаемого имени. Как недоверенное человекочитаемое поле, `director` SHALL NOT входить в plan-санитайзинг (он чистит `title`/`original_title`/`provider_hint`); очистка режиссёра применяется при рендере имени. Пустой `director` SHALL быть штатным (режиссёр неизвестен). #### Scenario: План сериала с per-file нумерацией - **GIVEN** сезон-пак из 10 видеофайлов - **WHEN** LLM возвращает план - **THEN** `type` = `series`, а каждый видеофайл несёт свои `season`/`episode` #### Scenario: Номер файла резолвится в путь - **GIVEN** список из 180 файлов, напечатанный в промпте - **WHEN** модель возвращает элемент плана с номером 13 - **THEN** система подставляет в `src` путь тринадцатого файла своего списка #### Scenario: Номер вне диапазона отбрасывает элемент, а не план - **GIVEN** раздача из 180 файлов и ответ модели, где один элемент несёт номер 181, а остальные 179 корректны - **WHEN** план разбирается - **THEN** негодный элемент отброшен, остальные 179 файлов остаются в плане - **AND** задача уходит в `review` с причиной, называющей отброшенный элемент - **AND** повторный запрос к модели не выполняется #### Scenario: Повторная адресация одного файла - **GIVEN** ответ модели, где два элемента адресуют один и тот же файл - **WHEN** план разбирается - **THEN** в плане остаётся первый элемент, второй отброшен - **AND** задача уходит в `review` с причиной, называющей повторную адресацию #### Scenario: Все элементы негодны — план не разобран - **GIVEN** ответ модели, где ни один элемент `files[]` не адресует реальный файл - **WHEN** план разбирается - **THEN** план не принимается как валидный #### Scenario: Путь вместо номера принимается как запасной формат - **GIVEN** ответ модели, где элемент `files[]` несёт путь файла вместо номера - **WHEN** план разбирается - **THEN** разбор успешен, если путь совпадает с реальным файлом торрента - **AND** correction-ретрай не выполняется #### Scenario: Номер и путь одновременно — главенствует номер - **GIVEN** элемент `files[]`, несущий и номер, и путь, которые указывают на разные файлы - **WHEN** план разбирается - **THEN** `src` берётся резолвом номера - **AND** задача уходит в `review` с причиной о несовпадении присланного пути с резолвом #### Scenario: Несуществующий src отклоняется - **GIVEN** ответ LLM, где `files[].src` не совпадает ни с одним файлом торрента - **WHEN** план разбирается - **THEN** такой элемент отбрасывается с причиной, а посторонний путь в план не попадает #### Scenario: Нумерация не зависит от вызывающего - **GIVEN** два вызова распознавания одной раздачи, получившие список файлов в разном порядке - **WHEN** собирается промпт - **THEN** напечатанная нумерация в обоих вызовах одинакова #### Scenario: Режиссёр не требуется от LLM и не влияет на гейт - **GIVEN** ответ LLM без поля `director` - **WHEN** план разбирается и оценивается - **THEN** разбор успешен, `director` пуст - **AND** отсутствие режиссёра не влияет на структурную валидацию и решение auto/review ### Requirement: Провайдер LLM за абстракцией со структурированным выводом Доступ к LLM SHALL быть за интерфейсом с выбором реализации по полю `[llm].type` (первый тип — `openai-compat`). Система SHALL запрашивать JSON-режим (`response_format: {"type":"json_object"}`), срезать ```-ограждения и валидировать ответ в Go против схемы плана. При ошибке разбора система SHALL ретраить до `[llm].max_retries`, передавая модели саму ошибку и схему. Если после ретраев ответ не разобран, задача SHALL уходить в `review` (НЕ в `failed`) с причиной «ответ LLM не разобран». Повторный запрос SHALL NOT переприсылать список файлов раздачи: список назван в первом сообщении диалога, и его номера сохраняют смысл на всех попытках. Размер запроса SHALL NOT расти пропорционально числу попыток. Признак завершения генерации (`finish_reason`) SHALL доходить до распознавания. Ответ, оборванный по длине, система SHALL уводить в `review` с отдельной причиной «ответ модели обрезан» и SHALL NOT повторять запрос ни тем же промптом, ни его вариантом: обрыв означает, что ответ не поместился, а не что модель ошиблась. #### Scenario: Неразобранный ответ уходит в review - **GIVEN** LLM, чей ответ не проходит валидацию схемы после всех ретраев - **WHEN** завершается распознавание - **THEN** задача переходит в `review` с причиной «ответ LLM не разобран» - **AND** задача НЕ переходит в `failed` #### Scenario: Повторная попытка не переприсылает список файлов - **GIVEN** раздача из 180 файлов и ответ модели с ошибкой разбора - **WHEN** выполняется correction-ретрай - **THEN** список файлов раздачи повторно не печатается - **AND** размер запроса второй попытки сопоставим с размером первой #### Scenario: Обрыв по длине не ретраится - **GIVEN** ответ модели с признаком обрыва генерации по длине - **WHEN** завершается распознавание - **THEN** задача переходит в `review` с причиной «ответ модели обрезан» - **AND** повторных запросов к модели не выполняется ### Requirement: Модель уверенности и решение auto/review Система SHALL раскладывать автоматически (без review) только при выполнении ВСЕГО: (1) подтверждённый единичный сильный матч в базе (`metadata-match`) с `provider_id`; (2) структурная валидация без предупреждений (фильм — ровно один основной видеофайл; сериал — число серий бьётся с базой, нумерация S·E консистентна); (3) согласованность пред-парса и LLM по типу/названию/году. Иначе задача SHALL уходить в `review` с явной причиной. Самооценку LLM (`confidence`) система SHALL учитывать лишь как вспомогательный сигнал, НЕ как единственный гейт. Список причин распознавания SHALL быть неоднородным: кроме блокирующих причин он MAY содержать информационные строки, которые показываются человеку и сохраняются вместе с распознаванием, но авто-раскладку НЕ отменяют — такова сводка покрытия плана (см. «Причины по нумерации серий и покрытию плана»). Поэтому решение auto/review система SHALL считать по **блокирующим** причинам, а НЕ по длине списка причин: непустой список сам по себе SHALL NOT означать `review`. #### Scenario: Нет матча в базе — всегда review - **GIVEN** план без подтверждённого матча в базе (база выключена или матча нет) - **WHEN** принимается решение auto/review - **THEN** задача уходит в `review`, авто-раскладка не делается #### Scenario: Матч и чистая валидация — авто - **GIVEN** подтверждённый единичный матч, чистая структурная валидация и согласованность сигналов - **WHEN** принимается решение - **THEN** допускается авто-раскладка (при отсутствии `force_review`) #### Scenario: Информационная причина авто не отменяет - **GIVEN** подтверждённый матч и чистая валидация, но покрытие плана неполно только за счёт файлов-спутников - **WHEN** принимается решение auto/review - **THEN** список причин непуст — в нём сводка покрытия - **AND** авто-раскладка допускается ## ADDED Requirements ### Requirement: Полный список файлов раздачи в промпте Промпт распознавания SHALL включать все файлы раздачи, а не фиксированную их часть. Предельное число файлов и предельный размер ответа модели SHALL задаваться настройками `[recognition].max_files` и `[recognition].max_tokens`; предел числа файлов служит предохранителем от аномальной раздачи, а не рабочим ограничением, и его значение по умолчанию SHALL соответствовать размеру промпта, который заведомо принимает модель. Если список всё же усечён пределом, распознавание SHALL называть усечение отдельной причиной ухода в `review`. Отказ модели, вызванный размером запроса, распознавание SHALL называть причиной о размере запроса, а не текстом провайдера. #### Scenario: Раздача на сотни файлов показана модели целиком - **GIVEN** раздача из 180 файлов и `max_files` = 500 - **WHEN** собирается промпт распознавания - **THEN** в списке файлов промпта присутствуют все 180 файлов #### Scenario: Усечение названо причиной - **GIVEN** раздача, число файлов которой превышает `max_files` - **WHEN** завершается распознавание - **THEN** задача уходит в `review`, и среди причин названо усечение списка файлов ### Requirement: Причины по нумерации серий и покрытию плана компактны Причины ухода в `review`, порождённые нумерацией серий, SHALL быть свёрнуты: на сезон приходится не более одной причины, перечисляющей недостающие серии. Распознавание SHALL называть покрытие плана — сколько файлов раздачи попало в план из общего числа, — когда покрыты не все файлы. Блокировать авто-раскладку SHALL только непокрытый видеофайл: файл, который модель не адресовала и который по расширению является видео, означает потерянную серию или фильм. Непокрытые файлы-спутники (субтитры, изображения, тексты, служебные файлы) SHALL показываться в покрытии, но SHALL NOT входить в структурную валидацию и SHALL NOT влиять на решение auto/review: модель вправе не перечислять то, что не раскладывается. #### Scenario: Пропуски сезона свёрнуты в одну причину - **GIVEN** план сезона, где недостают серии E05, E07 и E11 - **WHEN** формируются причины ухода в review - **THEN** этому сезону соответствует одна причина, перечисляющая недостающие серии #### Scenario: Непокрытый видеофайл блокирует авто - **GIVEN** раздача, где один видеофайл не адресован ни одним элементом плана - **WHEN** принимается решение auto/review - **THEN** задача уходит в `review` с причиной о непокрытом видеофайле #### Scenario: Непокрытые файлы-спутники авто не блокируют - **GIVEN** раздача, где не адресованы только `.nfo`, скриншоты и текстовый файл, а все видеофайлы покрыты, матч подтверждён и валидация чиста - **WHEN** принимается решение auto/review - **THEN** авто-раскладка допускается - **AND** покрытие плана показано человеку, но причиной ухода в review не является