Команда 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>
65 lines
4.9 KiB
Markdown
65 lines
4.9 KiB
Markdown
## 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`».
|