## ADDED Requirements ### Requirement: Вырожденное имя раздачи не считается именем Система SHALL нормализовать имя раздачи из `.torrent` **на границе разбора источника** (`internal/torrent`) и не считать содержательным именем вырожденное значение `-` — тот же литерал, которым экосистема обозначает «имени нет» (в библиотеке разбора он выставлен константой `metainfo.NoName` и присваивается при авторинге раздачи из вырожденного пути). Раздача объявляет это значение **сама**, полем `name`; библиотека его не синтезирует, поэтому речь о разборе объявленного значения, а не о фильтре чужого сентинела. Раздача, у которой поля `name` нет вовсе, уже даёт пустое имя — это отдельный случай, и он нормализации не требует. Оба случая после разбора SHALL быть неразличимы: имени нет. Нормализация SHALL применяться к **имени раздачи** и охватывать три поля, которые из него выводятся: - строка названия в синтезированном контексте распознавания (см. «Приём источника из .torrent-файла») — за неимением имени строка названия отсутствует; - `source_ref` загрузки — работает прежний фолбек на имя присланного файла; - подсказка вывода отображаемого имени, которую воркер берёт из метаданных торрента (см. `download-tracking` «Добавление пойманной загрузки в qBittorrent») — подсказка остаётся пустой. Пути файлов раздачи нормализации **не** подлежат, и это осознанно. Вырожденное имя попасть в путь файла может — при пустом наборе сегментов путь берётся фолбеком из имени раздачи, ненормализованного. Допустимо потому, что путь файла читается только как число файлов и набор расширений (сигнал по дереву), а целевые пути раскладки строятся не из него, а из ответа qBittorrent (см. `file-layout`). Появится потребитель, читающий путь как путь, — нормализовать его надо там же, на границе разбора. Имя раздачи — недоверенный вход, а контекст распознавания читается **построчно**. Поэтому нормализация SHALL схлопывать в один пробел любые последовательности пробельных символов — включая разделители строк — и убирать краевые, чтобы имя не могло добавить в контекст строку, выглядящую как синтезированный нами факт. Схлопывание SHALL выполняться **до** сравнения с вырожденным значением, иначе имя вида `" - "` сравнение не пройдёт, а после схлопывания станет ровно вырожденным и уедет вниз по потоку. Тем же правилом уже обрабатывается комментарий торрента. Защитой от инъекции в промпт оно **не** является и таковой объявляться SHALL NOT: пользовательский текст контекста многострочен по замыслу и идёт в промпт как есть (см. «Синтез контекста распознавания из полей magnet»). Правило **не** однородно с фильтром заглушек-идентификаторов `dn` у magnet: там заглушка вида `*-topic-` отбрасывается только в контексте, а в подсказку вывода имени по-прежнему уходит (действующее требование «Синтез контекста распознавания из полей magnet»). Здесь правило строже, и распространение его на magnet — отдельное изменение, этим требованием оно не заказано. #### Scenario: Раздача объявила имя `-` - **GIVEN** `.torrent`, у которого поле `name` равно `-` либо становится равным `-` после схлопывания пробельного (например `" - "`) - **WHEN** система разбирает источник и синтезирует контекст распознавания - **THEN** имя раздачи после разбора пусто, строки названия в контексте нет - **AND** остальные строки-факты (размер, файлы, трекер, комментарий) синтезируются как обычно - **AND** подсказка вывода отображаемого имени пуста #### Scenario: Раздача без поля `name` - **GIVEN** `.torrent` без поля `name` - **WHEN** система разбирает источник - **THEN** имя раздачи пусто — так же, как у раздачи, объявившей `-` #### Scenario: Вырожденное имя даёт source_ref из имени файла - **GIVEN** `.torrent` с именем `-`, присланный файлом `Dune.torrent` - **WHEN** вызывается приём - **THEN** `source_ref` загрузки — `Dune.torrent`, а не `-` #### Scenario: Имя с переводом строки не добавляет строк в контекст - **GIVEN** `.torrent`, имя которого содержит перевод строки и текст, похожий на синтезированный факт - **WHEN** система синтезирует контекст распознавания - **THEN** имя занимает ровно одну строку названия - **AND** число строк-фактов в контексте такое же, как у раздачи с обычным именем ### Requirement: Результат приёма при ошибке пуст Приём SHALL возвращать транспорту **нулевой результат** на любом пути ошибки: идентификатор загрузки, инфохэши, состояние и признак дедупликации в этом случае не публикуются. Причина в том, что быстрый приём не создаёт наблюдаемых последствий раньше, чем становится способен вернуть успех: разбор источника, дедуп-чек и заведение загрузки идут до ответа, а всё, что может отказать после заведения (добавление в qBittorrent, вывод имени), выполняет воркер и на исход приёма не влияет. Контракт SHALL удерживаться **структурно** — одним местом обнуления результата при ненулевой ошибке, а не аккуратностью каждой ветки возврата: перечень веток растёт, и именно расхождение перечня с текстом породило исходный дефект. Транспорты SHALL опираться на этот контракт и SHALL NOT обещать пользователю или журналу идентификатор загрузки, которого нет. Корреляционный ключ публичного отказа приёма зависит от транспорта, и требование называет его поимённо: - **HTTP API и веб-UI** — `request_id` запроса (см. `docs/conventions/errors.md`, «Граница и трансляция»); - **Telegram** — корреляционного ключа у отказа приёма нет, и требование это фиксирует как сегодняшнее состояние, а не как цель: диагностика ищется по записи приёма (`capability=ingest`, `infohash`). Заводить Telegram-транспорту собственный идентификатор запроса это требование SHALL NOT. #### Scenario: Невалидный источник - **WHEN** приём получает источник, который не разбирается ни как magnet, ни как `.torrent` - **THEN** возвращается ошибка и нулевой результат (идентификатор загрузки пуст) #### Scenario: Сбой хранилища на заведении загрузки - **WHEN** приём получает валидный источник, но хранилище отказывает при дедуп-чеке или заведении загрузки - **THEN** возвращается ошибка и нулевой результат #### Scenario: Отказ приёма на HTTP-границе - **GIVEN** приём отказал по любой причине - **WHEN** HTTP-транспорт (REST или веб-форма) отвечает пользователю - **THEN** ответ несёт `request_id` запроса, а не идентификатор загрузки