Files
avandClaude Opus 4.8 1369a9cabe Приём: дедуп по target_missing/orphaned + стоп-кран «Закрыть»
Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в 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>
2026-07-10 20:15:37 +03:00

8.5 KiB
Raw Permalink Blame History

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 (повторный приём — свежая попытка; старая терминальная запись хешем не владеет)