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

5.3 KiB
Raw Permalink Blame History

1. Дедуп: reingest-blocking states (предотвращение дубля)

  • 1.1 В internal/store/download.go ввести множество reingest-blocking состояний (active ∪ {target_missing, orphaned}) и отдельный метод поиска по любому из хешей (напр. FindReingestBlockingByInfohash) с приоритетом активной записи над desync-записью. НЕ расширять общий findActiveByInfohash — на нём стоят ActivateIfNoOtherActive/ AddInfohashes, где «активная» ОБЯЗАНА значить строго не-терминальную (иначе ломается инвариант «≤1 активной»). Desync-проверка — устойчивый пред-рид ДО create, активный гард (CreateDownloadIfNoActive) остаётся про строго активные.
  • 1.2 В internal/ingest/ingest.go (Ingest/attached) при найденной desync-записи (target_missing/orphaned) вернуть её как Deduplicated «спящей»: без перевода в активное, без обращения к qBittorrent; добавить в результат приёма признак «требует relink/закрытия».
  • 1.3 Убедиться, что донесение недостающих хешей и атомарный гард создания (CreateDownloadIfNoActive) не крадут хеш у другой активной загрузки и что гонка двух приёмов на свежую desync-запись не рождает близнеца.
  • 1.4 Обновить ответы транспортов при attach на desync-запись: бот (internal/tgbot) и HTTP/веб — текст «существует как #id без цели — привяжите заново или закройте».

2. Команда «Закрыть» (dismiss) — универсальный стоп-кран

  • 2.1 В internal/store/download.go добавить рёбра allowedTransitions в cancelled из терминальных done/failed/reverted/target_missing/ orphaned (у нетерминальных цель уже есть; deleted исключён). Обновить граф-тесты (internal/store/transition_test.go): (а) РАСЩЕПИТЬ TestCancelDeferReachableFromEveryNonTerminalcancelled теперь цель из любого состояния кроме deleted (в т.ч. терминальных), deferred — по-прежнему только из не-терминальных (не расширять общий цикл наивно, иначе ложно потребует deferred из терминалов или оставит дырявый гард); (б) в TestSetStateAllowsDeclaredEdges добавить новые терминал→cancelled рёбра (переход обычным SetDownloadState, без ActivateIfNoOtherActive).
  • 2.2 В internal/worker/review.go реализовать отдельный обработчик «Закрыть» (не ветка Cancel, который отклоняет терминальные): перевод <любое, кроме deleted> → cancelled с error_code = "user_dismiss" и причиной в error_msg/логе. Только setState: qBittorrent не вызывать, хардлинки не трогать (в т.ч. из done/orphaned), source-preflight не выполнять; из deleted — отказ; из cancelled — короткозамкнуть без setState (иначе идемпотентный no-op перезапишет error_code, подменив причину прежнего Отклонить).
  • 2.3 Веб-UI: отдельная danger zone внизу страницы загрузки с кнопкой «Закрыть» (доступна из любого состояния, кроме deleted; htmx, деградация без JS, ошибка на htmx-пути = 200 + фрагмент). Рассмотреть подтверждение для done/терминальных.
  • 2.4 Telegram (internal/tgbot): добавить действие «Закрыть» для загрузок.

3. Спеки, документация, проверка

  • 3.1 Обновить описание статусов/переходов в docs/specs/database.md (рёбра <терминальные> → cancelled, error_code = "user_dismiss").
  • 3.2 task test и task lint зелёные; добавить тесты: дедуп-attach на target_missing/orphaned, повторный приём после cancelled/done заводит новую, команда «Закрыть» переводит в cancelled из разных состояний и НЕ трогает qBittorrent/хардлинки (в т.ч. из done), отказ из deleted.
  • 3.3 openspec validate dedup-target-missing-and-dismiss --strict.