Жизненный цикл: 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>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
## Why
|
||||
|
||||
Команда **Defer** («Позже») сейчас гардит только `IsTerminal()`, поэтому
|
||||
принимает и пре-источниковое состояние `catched` (торрент ещё НЕ добавлен в
|
||||
qBittorrent). Defer из `catched` уводит задачу в лимбо → необратимый `deleted`
|
||||
(находка ревью MAJOR-6):
|
||||
|
||||
- `catched → deferred`: `processCatched` листает только `catched` и задачу
|
||||
больше не видит → торрент никогда не добавится в qBittorrent.
|
||||
- Из `deferred` дальше тупик: `Apply` → «нет плана»; `Rerecognize`/`Refine` →
|
||||
`ensureSourceReady` не находит раздачу → сверка (`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.go` — `Defer` (гард против `catched`);
|
||||
`internal/store/download.go` — убрать `StateDeferred` из исходящих `catched`
|
||||
и уточнить комментарий инварианта `deferred`.
|
||||
- **Тесты:** `internal/worker/review_test.go` — Defer из `catched` отклоняется,
|
||||
из `review` по-прежнему работает; `internal/store/transition_test.go` —
|
||||
инвариант «`deferred` из каждого не-терминального, кроме `catched`».
|
||||
Reference in New Issue
Block a user