Files
jellybit/openspec/changes/archive/2026-07-17-defer-exclude-catched/design.md
T
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

45 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
Фикс простой (гард одной команды + снятие мёртвого ребра графа). Дизайн
фиксирует два решения, чтобы ревью дизайна прошло до кода.
## Решение 1: где гардить Defer
`Defer` (`review.go`) отличается от прочих команд ревью: он не использует
`requireReviewable` (review/deferred), а сознательно широк — паркует любую
не-терминальную задачу (downloading/completed/recognizing/review/linking/stuck/
deferred), потому что «отложить» осмысленно и для ещё качающейся задачи. Значит
фикс НЕ «сузить Defer до reviewable», а точечно исключить пре-источниковое
`catched`.
Гард добавляем в сам `Defer` после `GetDownload`, рядом с существующей проверкой
`IsTerminal()`: `if d.State == store.StateCatched { return ...ErrConflict }`.
`ErrConflict` уже маппится в `httpapi.classifyErr` → 409 «действие недоступно в
текущем состоянии» (см. docs/conventions/errors.md), новый sentinel не нужен.
Сообщение обёртки — операторская диагностика для логов; наружу транспорт отдаёт
нейтральный маппинг.
## Решение 2: граф переходов
`allowedTransitions[StateCatched]` содержит `StateDeferred`. После гарда это
ребро мёртвое (единственный переход в `deferred` — команда `Defer`). Убираем
`StateDeferred` из исходящих `catched`, чтобы граф оставался тесным
надмножеством реальных переходов (инвариант спеки «граф — источник истины о
легальности рёбер»). Обновляем комментарий инварианта `deferred` в download.go
(«из КАЖДОГО не-терминального» → «кроме пре-источникового catched») и тест
`transition_test.go`, который его закрепляет.
Проверка полноты пре-источниковых состояний: `catched` — единственное
не-терминальное состояние без раздачи в qBittorrent. От `downloading` и далее
раздача есть; приёмное падение до `downloading` — терминальный `failed`
(`qbit_add`), Defer его уже отклоняет через `IsTerminal()`. Значит достаточно
исключить `catched`.
## Границы scope
Не трогаем `processCatched`, `reconcile`, граф `deleted`: корень бага —
единственный вход в лимбо (`catched → deferred`), закрытие входа устраняет всю
цепочку. Альтернативы из файла задачи (резюме deferred в processCatched;
preflight «источника не было» → failed/qbit_add) не нужны — вердикт задачи
«простой фикс».