Files
jellybit/openspec/changes/archive/2026-07-08-state-transition-graph/tasks.md
T
avandClaude Opus 4.8 90fd8640ed Машина состояний: декларативный граф легальных переходов
Единый источник истины `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>
2026-07-08 09:09:17 +03:00

3.6 KiB
Raw Blame History

Tasks

1. Граф в store

  • 1.1 Объявить allowedTransitions map[State][]State (from → to) в internal/store/download.go рядом с terminalStates, с комментарием об источнике истины и правиле самоперехода. Заполнить по таблице из design.md.
  • 1.2 Построить обратное отображение to → {легальные from} (для SQL-предиката) как package-level var через хелпер-инвертор; включать сам to (самопереход).

2. Гейт в setState

  • 2.1 В setState добавить предикат AND state IN (<легальные источники для to>) до/рядом с существующим гардом терминальности; аргументы через placeholders.
  • 2.2 На n == 0 — диагностическое чтение текущего состояния (через sqlx.QueryerContext, если e его поддерживает) для точного сообщения: «not found» / «illegal transition → » / терминал без revive.
  • 2.3 Сверить PromoteCatched: ребро catched → downloading присутствует в графе; оставить его собственный гард state='catched', добавить комментарий-ссылку на граф.

3. Тест согласованности

  • 3.1 TestTransitionGraphWellFormed: все состояния в ключах и значениях графа — известные (из полного списка State); ни один список не содержит сам ключ (петли не перечисляются явно).
  • 3.2 TestSetStateAllowsDeclaredEdges: для набора объявленных не-revive рёбер (downloading→completed, review→linking, recognizing→review, …) SetDownloadState проходит.
  • 3.3 TestSetStateRejectsUndeclaredEdges: репрезентативные необъявленные (review→done, downloading→done, completed→linking) отклоняются, состояние не меняется.
  • 3.4 TestSelfTransitionAllowed: deferred→deferred проходит.
  • 3.5 TestTerminalReviveOnlyViaActivate: failed→downloading через SetDownloadState отклоняется, а через ActivateIfNoOtherActive (при свободном infohash) проходит.
  • 3.6 TestCancelDeferReachableFromEveryNonTerminal: инвариант generic-команд — для каждого не-терминального состояния cancelled — легальная цель, и deferred — легальная цель (кроме самого deferred, где это самопереход). Ловит класс дыры «забыли состояние» (напр. linking после краха).
  • 3.7 TestPreflightDesyncEdges: рёбра reconcileToReality из ревью/терминальных состояний при пропавшем источнике — review/deferred/reverted/cancelled → orphaned и → deleted — проходят.

4. Проверка отсутствия регрессий

  • 4.1 task test — весь набор зелёный (существующие тесты воркера/store — сеть безопасности против слишком тесного графа).
  • 4.2 task lint — 0 issues.
  • 4.3 openspec validate state-transition-graph --strict.