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

4.9 KiB
Raw Permalink Blame History

Why

Команда Defer («Позже») сейчас гардит только IsTerminal(), поэтому принимает и пре-источниковое состояние catched (торрент ещё НЕ добавлен в qBittorrent). Defer из catched уводит задачу в лимбо → необратимый deleted (находка ревью MAJOR-6):

  • catched → deferred: processCatched листает только catched и задачу больше не видит → торрент никогда не добавится в qBittorrent.
  • Из deferred дальше тупик: Apply → «нет плана»; Rerecognize/RefineensureSourceReady не находит раздачу → сверка (sourcePresent=false, targetPresent=false) выводит deleted, а у deleted НОЛЬ исходящих рёбер → задача необратима, хотя байты .torrent лежат в download_torrent.
  • Плюс deleted семантически неверен: у catched ничего не качалось и не раскладывалось.

Defer до появления источника бессмысленен: «отложить на потом» нечего — задача ещё не дошла до ревью. Пре-источниковое catched — единственное такое состояние (все состояния от downloading и далее уже имеют раздачу в qBittorrent; приёмное падение qbit_add терминально и Defer его уже отклоняет).

What Changes

  • Defer отклоняет пре-источниковое состояние catched с конфликтом (ErrConflict) и понятным сообщением: отложить можно только после добавления торрента в qBittorrent. Прочие не-терминальные состояния (downloading/ completed/recognizing/review/linking/stuck/deferred), где раздача уже есть, Defer принимает как и раньше.
  • Граф переходов теряет ребро catched → deferred — раз Defer его больше не выполняет, ребро мёртвое; граф остаётся тесным надмножеством реальных переходов. Инвариант «deferred — легальная цель из каждого не-терминального состояния» уточняется: кроме пре-источникового catched.
  • UI/HTTP уже не предлагает Defer для catched: кнопка «🕗 Позже» живёт только на экране ревью (review/deferred), карточка catched лишь самополлингом ждёт перехода в downloading. Прямой вызов Defer для catched теперь отклоняется доменным гардом; транспорт транслирует отказ по своему каналу: REST — 409 «действие недоступно в текущем состоянии» (classifyErr), веб-путь /ui/downloads/{id}/defer — PRG-редирект (303) на /review/{id}?err=… с нейтральным сообщением (как прочие отказы команд ревью).

Capabilities

New Capabilities

Нет.

Modified Capabilities

  • review: требование «Команды ревью и их эффекты» — уточняет допустимые исходные состояния команды Позже (Defer): любое не-терминальное, кроме пре-источникового catched (там нет раздачи и нечего откладывать).

Impact

  • Спеки: дельта review (одно MODIFIED-требование с негативным сценарием). Требование графа переходов в download-tracking (декларативное, конкретные рёбра не перечисляет) не меняется — снятие ребра catched → deferred из единого источника истины в коде ему не противоречит.
  • Код: internal/worker/review.goDefer (гард против catched); internal/store/download.go — убрать StateDeferred из исходящих catched и уточнить комментарий инварианта deferred.
  • Тесты: internal/worker/review_test.go — Defer из catched отклоняется, из review по-прежнему работает; internal/store/transition_test.go — инвариант «deferred из каждого не-терминального, кроме catched».