Files
avandClaude Opus 4.8 098695011f Жизненный цикл: Defer запрещён из пре-источникового catched (MAJOR-6)
Команда Defer гардила только IsTerminal() и потому принимала catched
(торрент ещё не добавлен в qBittorrent). Defer из catched уводил задачу
в лимбо → необратимый deleted: processCatched листает только catched и
больше её не подхватывал, а последующие команды через отсутствие
источника выводили deleted (ноль исходящих рёбер), хотя байты .torrent
лежат в download_torrent.

- Worker.Defer отклоняет catched с ErrConflict (транслируется в 409 /
  редирект с сообщением); прочие не-терминальные состояния, где раздача
  уже есть, принимает как раньше.
- Снято мёртвое ребро графа catched → deferred (allowedTransitions);
  инвариант «deferred из каждого не-терминального» уточнён: кроме
  пре-источникового catched. catched — единственное состояние без
  раздачи среди не-терминальных.
- Тесты: Defer из catched отклоняется и не меняет состояние; инвариант
  графа обновлён + негативная проверка ребра.
- OpenSpec: MODIFIED «Команды ревью и их эффекты» (review) с позитивным
  и негативным сценариями; change заархивирован, дельта влита в спеку.
- Беклог: закрыта review-major6-defer-catched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 21:39:01 +03:00

7.7 KiB

MODIFIED Requirements

Requirement: Команды ревью и их эффекты

Экран ревью SHALL предоставлять команды: Применить (создать хардлинки по эффективному плану), Уточнить (добавить подсказку → перераспознать), Распознать заново (повторный прогон без новой подсказки), Игнор файла, Позже (deferred), Отклонить (cancelled), Undo (снять созданные ссылки → reverted) и Привязать заново (из reverted/cancelled/target_missing → перераспознавание с ручным подтверждением). Экран ревью MUST NOT содержать команду переключения типа movie↔series: тип показывается read-only, а его корректировка выполняется мягкой подсказкой через Уточнить. Команды из любого транспорта SHALL сериализоваться worker'ом под единой блокировкой; применяется последняя валидная команда.

Команда Позже (Defer) SHALL парковать задачу в deferred из любого не-терминального состояния, у которого уже есть раздача в qBittorrent, и SHALL отклонять её из пре-источникового состояния catched (торрент ещё НЕ добавлен в qBittorrent) — конфликтом (ErrConflict) с понятным пользователю сообщением, НЕ меняя состояние загрузки. Пре-источниковое catched — единственное состояние без раздачи среди не-терминальных: откладывать в нём нечего (задача ещё не дошла до ревью), а catched → deferred уводил бы задачу в лимбо — processCatched листает только catched и больше её не подхватит, а последующие команды через отсутствие источника выводят необратимый deleted. Терминальные состояния Defer SHALL отклонять как и прежде (ErrConflict).

Команды, которым нужен источник (Применить, Уточнить, Распознать заново, Привязать заново, а также фиксация типа), SHALL синхронно (без дебаунса) проверять перед действием, что источник не только присутствует в qBittorrent, но и готов к раскладке — раздача в готовом классе состояния (uploading/stalledUP/pausedUP/… с учётом различий имён qBit v4/v5), т.е. файлы докачаны. Если источник ещё качается (любое downloading-подобное или переходное moving/checking состояние), команда SHALL отказывать с конфликтом и причиной «торрент ещё качается», НЕ создавая хардлинки и НЕ меняя состояние загрузки (её нахождение в review/deferred/… легитимно, приводить к реальности нечего). Отсутствие источника в qBittorrent SHALL по-прежнему приводить состояние к реальности (orphaned/deleted) и отказывать. Так недокачанная задача не может пройти через перераспознавание в авто-раскладку или ручное применение и захардлинкать неполные файлы, обойдя финальность состояния completed.

Scenario: Применение создаёт раскладку

  • GIVEN загрузка в review с эффективным планом
  • WHEN пользователь выбирает «Применить»
  • THEN создаются хардлинки по плану, задача переходит к раскладке

Scenario: Отклонить и привязать заново

  • GIVEN загрузка в review
  • WHEN пользователь «Отклонить», затем «Привязать заново»
  • THEN задача уходит в cancelled, а затем снова на распознавание с ручным подтверждением (авто-раскладка не делается)

Scenario: Тип не переключается кнопкой

  • GIVEN загрузка в review с распознанным типом
  • WHEN пользователь открывает экран ревью
  • THEN отдельной команды/кнопки переключения movie↔series на экране нет
  • AND тип показан read-only в инфо-части выбранного источника

Scenario: Позже паркует задачу из ревью

  • GIVEN загрузка в review (раздача в qBittorrent уже есть)
  • WHEN пользователь выбирает «Позже»
  • THEN задача переходит в deferred и возвращается на поверхность ревью по любому последующему действию

Scenario: Позже отклоняется для пре-источникового catched

  • GIVEN загрузка в catched (торрент ещё не добавлен в qBittorrent)
  • WHEN приходит команда «Позже» (Defer, напр. прямым POST на /ui/downloads/{id}/defer)
  • THEN команда отклоняется конфликтом с понятным сообщением, что отложить можно только после добавления торрента
  • AND загрузка остаётся в catched и штатно доходит до downloading через processCatched

Scenario: Недокачанный источник отклоняет перераспознавание

  • GIVEN загрузка припаркована в deferred, а её раздача в qBittorrent ещё качается (downloading, файлы не докачаны)
  • WHEN пользователь выбирает «Распознать заново» (или «Уточнить»/«Привязать заново»/фиксацию типа)
  • THEN команда отклоняется с конфликтом и причиной «торрент ещё качается»
  • AND загрузка остаётся в deferred, хардлинки не создаются, авто-раскладка не запускается

Scenario: Недокачанный источник отклоняет ручное применение

  • GIVEN загрузка в review, чья раздача в qBittorrent ещё качается
  • WHEN пользователь выбирает «Применить»
  • THEN команда отклоняется с конфликтом «торрент ещё качается», хардлинки на неполные файлы не создаются, состояние загрузки не меняется