Единый источник истины `allowedTransitions` (from → {разрешённые to}) в
internal/store; `setState` сверяет переход дополнительным SQL-предикатом
`state IN (<легальные источники>)` — необъявленное ребро (и не самопереход)
отклоняется атомарно, с точным сообщением. Гейт ортогонален гарду
терминальности: ребро из терминального состояния проходит только через
ActivateIfNoOtherActive. Без внешней библиотеки-FSM (обоснование — design.md).
Граф выведен построчно из воркера; ревью дизайна поймало 8 preflight-рёбер
(reconcileToReality → orphaned/deleted) и linking→cancel/defer после краха.
Тест-инвариант «cancel/defer достижимы из любого не-терминального» ловит класс
пропущенного ребра. Фикстуры тестов, форсившие состояния через SetDownloadState,
переведены на прямой UPDATE (forceState).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
3.6 KiB
Markdown
51 lines
3.6 KiB
Markdown
# Tasks
|
||
|
||
## 1. Граф в store
|
||
|
||
- [x] 1.1 Объявить `allowedTransitions map[State][]State` (from → to) в
|
||
`internal/store/download.go` рядом с `terminalStates`, с комментарием об источнике
|
||
истины и правиле самоперехода. Заполнить по таблице из `design.md`.
|
||
- [x] 1.2 Построить обратное отображение `to → {легальные from}` (для SQL-предиката)
|
||
как package-level `var` через хелпер-инвертор; включать сам `to` (самопереход).
|
||
|
||
## 2. Гейт в setState
|
||
|
||
- [x] 2.1 В `setState` добавить предикат `AND state IN (<легальные источники для to>)`
|
||
до/рядом с существующим гардом терминальности; аргументы через `placeholders`.
|
||
- [x] 2.2 На `n == 0` — диагностическое чтение текущего состояния (через
|
||
`sqlx.QueryerContext`, если `e` его поддерживает) для точного сообщения:
|
||
«not found» / «illegal transition <cur> → <to>» / терминал без revive.
|
||
- [x] 2.3 Сверить `PromoteCatched`: ребро `catched → downloading` присутствует в
|
||
графе; оставить его собственный гард `state='catched'`, добавить комментарий-ссылку
|
||
на граф.
|
||
|
||
## 3. Тест согласованности
|
||
|
||
- [x] 3.1 `TestTransitionGraphWellFormed`: все состояния в ключах и значениях графа —
|
||
известные (из полного списка `State`); ни один список не содержит сам ключ
|
||
(петли не перечисляются явно).
|
||
- [x] 3.2 `TestSetStateAllowsDeclaredEdges`: для набора объявленных не-revive рёбер
|
||
(`downloading→completed`, `review→linking`, `recognizing→review`, …)
|
||
`SetDownloadState` проходит.
|
||
- [x] 3.3 `TestSetStateRejectsUndeclaredEdges`: репрезентативные необъявленные
|
||
(`review→done`, `downloading→done`, `completed→linking`) отклоняются, состояние не
|
||
меняется.
|
||
- [x] 3.4 `TestSelfTransitionAllowed`: `deferred→deferred` проходит.
|
||
- [x] 3.5 `TestTerminalReviveOnlyViaActivate`: `failed→downloading` через
|
||
`SetDownloadState` отклоняется, а через `ActivateIfNoOtherActive` (при свободном
|
||
infohash) проходит.
|
||
- [x] 3.6 `TestCancelDeferReachableFromEveryNonTerminal`: инвариант generic-команд —
|
||
для каждого не-терминального состояния `cancelled` — легальная цель, и `deferred` —
|
||
легальная цель (кроме самого `deferred`, где это самопереход). Ловит класс дыры
|
||
«забыли состояние» (напр. `linking` после краха).
|
||
- [x] 3.7 `TestPreflightDesyncEdges`: рёбра `reconcileToReality` из ревью/терминальных
|
||
состояний при пропавшем источнике — `review/deferred/reverted/cancelled →
|
||
orphaned` и `→ deleted` — проходят.
|
||
|
||
## 4. Проверка отсутствия регрессий
|
||
|
||
- [x] 4.1 `task test` — весь набор зелёный (существующие тесты воркера/store — сеть
|
||
безопасности против слишком тесного графа).
|
||
- [x] 4.2 `task lint` — 0 issues.
|
||
- [x] 4.3 `openspec validate state-transition-graph --strict`.
|