Машина состояний: декларативный граф легальных переходов

Единый источник истины `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>
This commit is contained in:
av
2026-07-08 09:09:17 +03:00
co-authored by Claude Opus 4.8
parent 0d263270cb
commit 90fd8640ed
10 changed files with 716 additions and 25 deletions
+1 -3
View File
@@ -24,9 +24,7 @@ func mkDownload(t *testing.T, st *Store, n int, state State, display string) str
t.Fatalf("create #%d: unexpected dedup", n)
}
if state != StateDownloading {
if err := st.SetDownloadState(ctx, d.ID, state, "", ""); err != nil {
t.Fatalf("set state #%d: %v", n, err)
}
forceState(t, st, d.ID, state)
}
return d.ID
}