Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в target_missing: дедуп искал только активную задачу,
а target_missing терминален → заводилась новая загрузка, воркер усыновлял
уже присутствующий торрент и раскладывал его.
- Приём: критерий дедупа расширен до «блокирующей повторный приём» =
активные ∪ {target_missing, orphaned}. Повторный приём такого инфохэша
привязывается к существующей записи (спящей, без обращения к qBittorrent),
а не плодит близнеца. Прочие терминальные (done/cancelled/failed/reverted/
deleted) повторный приём не блокируют — осознанная свежая попытка. Новый
read-метод FindReingestBlockingByInfohash (приоритет активной над desync);
общий active-гард не тронут.
- Команда «Закрыть» (Dismiss) — универсальный стоп-кран из любого состояния,
кроме deleted → cancelled (error_code=user_dismiss). Только меняет статус:
файлы (в т.ч. хардлинки done/orphaned) и раздачу qBittorrent не трогает,
в отличие от «Удалить». Веб — danger-зона внизу страницы; Telegram —
кнопка с подтверждением; из cancelled — идемпотентный no-op.
- Транспорты при дедупе на desync-запись сообщают адресно (target_missing —
привязать заново/закрыть; orphaned — закрыть и добавить заново); веб при
дедупе ведёт на страницу существующей записи.
Спеки: ingest (дедуп), state-reconciliation (стоп-кран); граф переходов
допополнен рёбрами <терминал>→cancelled. OpenSpec change
dedup-target-missing-and-dismiss заархивирован.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.5 KiB
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(повторный приём — свежая попытка; старая терминальная запись хешем не владеет)