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

59 lines
5.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 1. Дедуп: reingest-blocking states (предотвращение дубля)
- [x] 1.1 В `internal/store/download.go` ввести множество reingest-blocking
состояний (active ∪ `{target_missing, orphaned}`) и **отдельный** метод
поиска по любому из хешей (напр. `FindReingestBlockingByInfohash`) с
приоритетом активной записи над desync-записью. НЕ расширять общий
`findActiveByInfohash` — на нём стоят `ActivateIfNoOtherActive`/
`AddInfohashes`, где «активная» ОБЯЗАНА значить строго не-терминальную
(иначе ломается инвариант «≤1 активной»). Desync-проверка — устойчивый
пред-рид ДО create, активный гард (`CreateDownloadIfNoActive`) остаётся про
строго активные.
- [x] 1.2 В `internal/ingest/ingest.go` (`Ingest`/`attached`) при найденной
desync-записи (`target_missing`/`orphaned`) вернуть её как `Deduplicated`
«спящей»: без перевода в активное, без обращения к qBittorrent; добавить
в результат приёма признак «требует relink/закрытия».
- [x] 1.3 Убедиться, что донесение недостающих хешей и атомарный гард создания
(`CreateDownloadIfNoActive`) не крадут хеш у другой активной загрузки и
что гонка двух приёмов на свежую desync-запись не рождает близнеца.
- [x] 1.4 Обновить ответы транспортов при attach на desync-запись: бот
(`internal/tgbot`) и HTTP/веб — текст «существует как #id без цели —
привяжите заново или закройте».
## 2. Команда «Закрыть» (dismiss) — универсальный стоп-кран
- [x] 2.1 В `internal/store/download.go` добавить рёбра `allowedTransitions` в
`cancelled` из терминальных `done`/`failed`/`reverted`/`target_missing`/
`orphaned` (у нетерминальных цель уже есть; `deleted` исключён). Обновить
граф-тесты (`internal/store/transition_test.go`): (а) РАСЩЕПИТЬ
`TestCancelDeferReachableFromEveryNonTerminal``cancelled` теперь цель из
любого состояния кроме `deleted` (в т.ч. терминальных), `deferred`
по-прежнему только из не-терминальных (не расширять общий цикл наивно, иначе
ложно потребует `deferred` из терминалов или оставит дырявый гард);
(б) в `TestSetStateAllowsDeclaredEdges` добавить новые терминал→`cancelled`
рёбра (переход обычным `SetDownloadState`, без `ActivateIfNoOtherActive`).
- [x] 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`, подменив
причину прежнего `Отклонить`).
- [x] 2.3 Веб-UI: отдельная danger zone внизу страницы загрузки с кнопкой
«Закрыть» (доступна из любого состояния, кроме `deleted`; htmx, деградация
без JS, ошибка на htmx-пути = 200 + фрагмент). Рассмотреть подтверждение
для `done`/терминальных.
- [x] 2.4 Telegram (`internal/tgbot`): добавить действие «Закрыть» для загрузок.
## 3. Спеки, документация, проверка
- [x] 3.1 Обновить описание статусов/переходов в `docs/specs/database.md`
(рёбра `<терминальные> → cancelled`, `error_code = "user_dismiss"`).
- [x] 3.2 `task test` и `task lint` зелёные; добавить тесты: дедуп-attach на
`target_missing`/`orphaned`, повторный приём после `cancelled`/`done`
заводит новую, команда «Закрыть» переводит в `cancelled` из разных
состояний и НЕ трогает qBittorrent/хардлинки (в т.ч. из `done`), отказ из
`deleted`.
- [x] 3.3 `openspec validate dedup-target-missing-and-dismiss --strict`.