Вторая половина «единого окна»: команда «Удалить» снимает наши библиотечные хардлинки (гард последней копии осознанно выключен, в отличие от Undo) и сносит раздачу с файлами из qBittorrent (deleteFiles=true) → терминальный deleted. Доступна из done/orphaned/target_missing, идемпотентна к отсутствующей стороне; инициатор различается через error_code=user_delete. Подтверждение обязательно: веб — danger-секция внизу страницы (hx-confirm + details), Telegram — двухшаговый inline-confirm. qbt.Delete + layout.Remove (unlink без ErrLastCopy, только свои ссылки под movies/series). Граф переходов не менялся — рёбра уже были. OpenSpec: state-reconciliation +1 требование; синк workflow.md; беклог закрыт. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.6 KiB
ADDED Requirements
Requirement: Полное удаление загрузки пользователем
Система SHALL предоставлять пользователю команду «Удалить» (delete),
доступную из состояний done, orphaned и target_missing во всех транспортах
(веб-UI и Telegram, опц. REST). Команда SHALL снимать обе стороны загрузки —
целевые библиотечные хардлинки И раздачу с файлами в qBittorrent — и переводить
задачу в терминальное deleted. Из прочих состояний команда доступна SHALL NOT.
Снятие цели SHALL идти по механике снятия ссылок последнего батча (как в Undo:
superseded пропускаются как забранные другой загрузкой), но отдельным путём с
выключенным гардом последней копии — не переиспользуя guarded-Undo: в отличие
от Undo, delete SHALL снимать целевую ссылку, даже если она — последняя копия
данных (nlink <= 1). Это осознанный выход за
инвариант «источник неприкосновенен», поэтому delete SHALL требовать явного
подтверждения пользователя перед выполнением и SHALL NOT срабатывать по
одиночному клику/тапу. Снятие цели SHALL затрагивать только собственные ссылки
загрузки строго под paths.movies/series; файлы источника под
paths.downloads система сама трогать SHALL NOT — их удаляет qBittorrent по
вызову API с deleteFiles=true.
В отличие от прочих команд, требующих источника, delete синхронный source-preflight
выполнять SHALL NOT и под требование «Принудительная проверка источника/цели перед
действием» не подпадает: цель delete — снять источник, поэтому его отсутствие
трактуется как уже снятая сторона, а не как повод привести состояние сверкой и
отказать. Удаление SHALL быть идемпотентным к отсутствующей стороне: в orphaned
(нет источника) отсутствие раздачи в qBittorrent ошибкой считаться SHALL NOT; в
target_missing (нет цели) пустой список живых ссылок обрабатывается как «нечего
снимать». Если qBittorrent вернул ошибку при удалении присутствующей раздачи,
система в deleted переходить SHALL NOT (не заявляем освобождение места, которого
не произошло), SHALL сообщить пользователю причину отказа (это не ErrConflict,
а ошибка внешнего сервиса — транслируется как таковая), и повторный delete
идемпотентно дожимает удаление, опираясь на оставшийся done либо приведённый
сверкой к реальности target_missing (кратковременное рассогласование до тика
сверки ожидаемо).
Инициатора перехода в deleted система SHALL отличать от фоновой сверки:
пользовательское удаление SHALL помечаться error_code = "user_delete" (сверка
кладёт "reconcile"), человекочитаемую причину — в error_msg и лог перехода.
Новый статус для этого система вводить SHALL NOT — переиспользуется существующее
терминальное deleted (сверка его не переоценивает, см. требование о deleted).
Scenario: Удаление из done снимает обе стороны и освобождает место
- GIVEN задача в
done: раздача присутствует в qBittorrent, её библиотечные хардлинки существуют - WHEN пользователь подтверждает «Удалить»
- THEN библиотечные ссылки последнего батча снимаются
- AND раздача с файлами удаляется из qBittorrent (
deleteFiles=true) - AND задача переходит в
deletedсerror_code = "user_delete"
Scenario: Удаление из orphaned снимает последнюю копию осознанно
- GIVEN задача в
orphaned: источник пропал, библиотечный хардлинк остался единственной копией данных (nlink <= 1) - WHEN пользователь подтверждает «Удалить»
- THEN библиотечная ссылка снимается несмотря на то, что она последняя копия
(гард последней копии выключен, в отличие от
Undo) - AND отсутствие раздачи в qBittorrent ошибкой не считается
- AND задача переходит в
deleted
Scenario: Удаление из target_missing сносит остаточную раздачу
- GIVEN задача в
target_missing: источник присутствует, цель уже удалена вручную - WHEN пользователь подтверждает «Удалить»
- THEN снятие цели идемпотентно (живых ссылок нет)
- AND раздача с файлами удаляется из qBittorrent
- AND задача переходит в
deleted
Scenario: Удаление требует подтверждения
- GIVEN задача в
done - WHEN пользователь инициирует «Удалить», но не подтверждает действие
- THEN ни ссылки, ни раздача не удаляются, состояние остаётся
done
Scenario: Удаление недоступно из прочих состояний
- GIVEN задача в
review(или ином состоянии внеdone/orphaned/target_missing) - WHEN приходит команда «Удалить»
- THEN команда отклоняется с конфликтом, состояние не меняется
Scenario: Ошибка qBittorrent не метит deleted ложно
- GIVEN задача в
done, раздача присутствует, но qBittorrent вернул ошибку на удаление - WHEN пользователь подтверждает «Удалить»
- THEN задача в
deletedне переходит (место не освобождено) - AND пользователю сообщается причина отказа (ошибка qBittorrent, не тихий успех)
- AND повторный delete идемпотентно дожимает удаление