## MODIFIED Requirements ### Requirement: Приём источника и заведение загрузки Приём SHALL быть единым **быстрым** use-case, общим для всех транспортов (HTTP, Telegram, CLI): по источнику (Ф1 — magnet) и текстовому контексту система SHALL синхронно извлечь инфохэши, синтезировать контекст из полей ссылки (без сети), дедуплицировать по **блокирующей повторный приём** задаче (активной либо удерживающей источник ради незакрытого намерения — `target_missing`/`orphaned`; см. «Дедупликация приёма по любому из хешей») и при отсутствии дубля завести загрузку (`download` в состоянии **`catched`** + записи `download_infohash`), после чего **сразу вернуть ответ** транспорту. Заведение загрузки и запись её хешей SHALL выполняться атомарно (см. «Атомарность возврата загрузки в активное состояние»). Синхронный путь приёма SHALL NOT обращаться к qBittorrent и SHALL NOT выводить отображаемое имя (потенциально медленный LLM): и добавление источника в qBittorrent, и вывод имени выполняются отдельным асинхронным шагом машины состояний (worker) — см. `download-tracking` «Добавление пойманной загрузки в qBittorrent». `catched` — нетерминальное активное состояние: оно участвует в инварианте «не более одной активной загрузки на infohash» наравне с прочими активными. #### Scenario: Быстрый приём magnet - **GIVEN** валидная magnet-ссылка и контекст - **WHEN** вызывается приём - **THEN** создаётся `download` в состоянии `catched` с записями `download_infohash` - **AND** ответ транспорту отдан без обращения к qBittorrent и без вывода имени #### Scenario: Дубль по активной задаче на быстром пути - **GIVEN** уже есть активная (в т.ч. `catched`) загрузка с тем же infohash - **WHEN** вызывается приём - **THEN** новая загрузка не создаётся, возвращается существующая ### Requirement: Дедупликация приёма по любому из хешей При приёме система SHALL искать загрузку, **блокирующую повторный приём**, по любому из известных хешей и, найдя, SHALL возвращать её вместо создания новой. Блокирующими SHALL считаться загрузки в активном (нетерминальном) состоянии **либо** удерживающие источник ради незакрытого намерения — `target_missing` (источник жив в qBittorrent, ждёт relink) и `orphaned` (источник пропал, запись держит претензию на последнюю копию). Прочие терминальные состояния (`done`, `cancelled`, `failed`, `reverted`, `deleted`) блокирующими быть SHALL NOT: повторный приём такого инфохэша — осознанное «хочу заново» и SHALL заводить новую загрузку. Когда найденная блокирующая загрузка терминальна (`target_missing`/`orphaned`), приём SHALL возвращать её как существующую (`Deduplicated`) **спящей**: система SHALL NOT переводить её в активное состояние и SHALL NOT обращаться к qBittorrent (перепривязка — отдельное явное действие пользователя, а не побочный эффект приёма); ответ транспорту SHALL сообщать, что запись существует и требует перепривязки либо закрытия. Атомарный инвариант касается **активной** составляющей: проверка отсутствия другой активной загрузки на любом из хешей и вставка новой загрузки с её хешами SHALL выполняться в одной write-транзакции, поддерживая «не более одной активной загрузки на infohash» (тот же общий active-гард, что у прочих путей активации). Расширение критерия на desync-состояния (`target_missing`/`orphaned`) SHALL быть устойчивым пред-ридом до создания, коротко замыкающим приём на возврат существующей записи; desync-состояния в общий active-гард заводиться SHALL NOT (их терминальность оставляет `state`-инвариант «активности» нетронутым). Отдельного снимаемого/восстанавливаемого ключа идемпотентности в схеме быть SHALL NOT — активность выводится только из `state`. #### Scenario: Повторный приём при активной загрузке - **GIVEN** активная загрузка с infohash `h` - **WHEN** принимается magnet с тем же `h` - **THEN** новая загрузка не создаётся, возвращается существующая #### Scenario: Повторный приём при записи без цели - **GIVEN** загрузка с infohash `h` в `target_missing` (источник жив, цель удалена) - **WHEN** принимается magnet с тем же `h` - **THEN** новая загрузка не создаётся, возвращается существующая запись как `Deduplicated` - **AND** её состояние остаётся `target_missing` (в активное не переводится, к qBittorrent обращения нет) - **AND** ответ транспорту указывает, что запись существует и её нужно привязать заново или закрыть #### Scenario: Повторный приём при осиротевшей записи - **GIVEN** загрузка с infohash `h` в `orphaned` (источник пропал) - **WHEN** принимается magnet/torrent с тем же `h` - **THEN** новая загрузка не создаётся, возвращается существующая запись как `Deduplicated` #### Scenario: Повторный приём после завершения - **GIVEN** загрузка с infohash `h` в терминальном состоянии `done` - **WHEN** принимается magnet с тем же `h` - **THEN** создаётся новая загрузка со своим ULID и записью `h` #### Scenario: Повторный приём после закрытия записи - **GIVEN** загрузка с infohash `h` в `cancelled` (в т.ч. закрытая из `target_missing`) - **WHEN** принимается magnet с тем же `h` - **THEN** создаётся новая загрузка со своим ULID и записью `h` #### Scenario: Повторный приём при прочих терминальных состояниях - **GIVEN** загрузка с infohash `h` в `failed` или `reverted` (не удерживает источник ради незакрытого намерения) - **WHEN** принимается magnet/torrent с тем же `h` - **THEN** создаётся новая загрузка со своим ULID и записью `h` (повторный приём — свежая попытка; старая терминальная запись хешем не владеет)