Ревью: preflight готовности источника для команд ревью (MAJOR-5)

Команды ревью проверяли только наличие раздачи в qBittorrent, но не её
готовность. Недокачанную задачу можно припарковать в deferred, затем
«Распознать заново» → recognizing → авто-раскладка (Rerecognize/Refine/
SetType не ставят force_review) → хардлинки на неполные файлы. Даже ручной
Apply не имел preflight завершённости.

Вводим ensureSourceReady (classify(t.State)==classReady) вместо
ensureSourcePresent во всех командах, которым нужен источник (Relink/
Rerecognize/Refine/SetType), и inline-проверку класса в Apply — последний
рубеж перед хардлинками. Недокачанный источник → отдельный sentinel
ErrNotReady (409) с actionable-текстом «торрент ещё качается» в web и
Telegram, без reconcile (состояние deferred/review легитимно).

Change review-readiness-preflight заархивирован, дельта влита в
openspec/specs/review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-08 16:40:49 +03:00
co-authored by Claude Opus 4.8
parent a43adea723
commit b1bca98738
16 changed files with 475 additions and 41 deletions
+20 -10
View File
@@ -245,23 +245,33 @@ func recoveredState(state string) store.State {
// --- Синхронный preflight перед действием (не доверяем state в БД) ---
// ensureSourcePresent синхронно (без дебаунса) проверяет, что раздача есть в
// qBittorrent прямо сейчас. При отсутствии приводит состояние к реальности и
// возвращает ErrConflict. Недоступность qBittorrent — честный отказ операции.
func (w *Worker) ensureSourcePresent(ctx context.Context, d *store.Download, op string) error {
// ensureSourceReady синхронно (без дебаунса) проверяет, что раздача есть в
// qBittorrent прямо сейчас И докачана (класс classReady). Команды ревью, ведущие
// к распознаванию или раскладке, работают только с готовым источником — иначе
// хардлинки легли бы на неполные файлы (qBittorrent отдаёт имена до завершения).
// - источник исчез → приводим состояние к реальности (orphaned/deleted) и
// ErrConflict;
// - источник есть, но ещё качается → ErrNotReady БЕЗ reconcile: нахождение
// задачи в review/deferred/… легитимно, приводить нечего.
//
// Недоступность qBittorrent — честный отказ операции.
func (w *Worker) ensureSourceReady(ctx context.Context, d *store.Download, op string) error {
if len(d.Infohashes) == 0 {
return fmt.Errorf("%s: download %s has no infohash", op, d.ID)
}
_, ok, err := w.torrentByInfohash(ctx, d.HashList())
t, ok, err := w.torrentByInfohash(ctx, d.HashList())
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
if ok {
return nil
if !ok {
// Источник пропал — немедленно приводим состояние к реальности.
w.reconcileToReality(ctx, *d, false)
return fmt.Errorf("%s: источник удалён из qBittorrent: %w", op, ErrConflict)
}
// Источник пропал — немедленно приводим состояние к реальности.
w.reconcileToReality(ctx, *d, false)
return fmt.Errorf("%s: источник удалён из qBittorrent: %w", op, ErrConflict)
if classify(t.State) != classReady {
return fmt.Errorf("%s: торрент ещё качается: %w", op, ErrNotReady)
}
return nil
}
// reconcileToReality выводит и проставляет состояние по уже известному факту об