Ревью: 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:
@@ -247,6 +247,11 @@ func (w *Worker) Apply(ctx context.Context, id string) error {
|
||||
w.reconcileToReality(ctx, *d, false)
|
||||
return fmt.Errorf("apply: источник удалён из qBittorrent: %w", ErrConflict)
|
||||
}
|
||||
if classify(t.State) != classReady {
|
||||
// Последний рубеж перед хардлинками: источник ещё качается — не
|
||||
// линкуем неполные файлы (задача могла войти в review иным путём).
|
||||
return fmt.Errorf("apply: торрент ещё качается: %w", ErrNotReady)
|
||||
}
|
||||
|
||||
w.transition(ctx, *d, store.StateLinking, "", "")
|
||||
if err := w.linkPlan(ctx, d, plan, tag, translatePath(t.SavePath, w.cfg.PathMap)); err != nil {
|
||||
@@ -321,7 +326,7 @@ func (w *Worker) linkPlan(ctx context.Context, d *store.Download, plan recognize
|
||||
// (cancelled) задачу: возвращает её на распознавание, и поллинг-цикл
|
||||
// перезапустит recognize. Авто-раскладку при этом не делаем — ручная
|
||||
// перепривязка всегда проходит через ревью с подтверждением (force_review).
|
||||
// Источник (раздача в qBittorrent) для этого должен быть на месте.
|
||||
// Источник (раздача в qBittorrent) для этого должен быть на месте и докачан.
|
||||
func (w *Worker) Relink(ctx context.Context, id string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
@@ -333,9 +338,10 @@ func (w *Worker) Relink(ctx context.Context, id string) error {
|
||||
if d.State != store.StateReverted && d.State != store.StateCancelled && d.State != store.StateTargetMissing {
|
||||
return fmt.Errorf("relink: download %s is in state %s (expected reverted/cancelled/target_missing): %w", id, d.State, ErrConflict)
|
||||
}
|
||||
// Источник нужен для распознавания — проверяем синхронно (без дебаунса) и при
|
||||
// его отсутствии приводим состояние к реальности (orphaned/deleted).
|
||||
if err := w.ensureSourcePresent(ctx, d, "relink"); err != nil {
|
||||
// Источник нужен для распознавания и должен быть докачан — проверяем
|
||||
// синхронно (без дебаунса); отсутствие приводит состояние к реальности
|
||||
// (orphaned/deleted), недокачанный — ErrNotReady.
|
||||
if err := w.ensureSourceReady(ctx, d, "relink"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Ручная перепривязка — всегда с подтверждением, без авто-раскладки.
|
||||
@@ -366,7 +372,7 @@ func (w *Worker) Rerecognize(ctx context.Context, id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.ensureSourcePresent(ctx, d, "rerecognize"); err != nil {
|
||||
if err := w.ensureSourceReady(ctx, d, "rerecognize"); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = w.scoped(ctx, capReview, id, d.PrimaryInfohash())
|
||||
@@ -388,7 +394,7 @@ func (w *Worker) Refine(ctx context.Context, id string, hint string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.ensureSourcePresent(ctx, d, "refine"); err != nil {
|
||||
if err := w.ensureSourceReady(ctx, d, "refine"); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = w.scoped(ctx, capReview, id, d.PrimaryInfohash())
|
||||
@@ -413,7 +419,7 @@ func (w *Worker) SetType(ctx context.Context, id string, mediaType string) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.ensureSourcePresent(ctx, d, "set type"); err != nil {
|
||||
if err := w.ensureSourceReady(ctx, d, "set type"); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = w.scoped(ctx, capReview, id, d.PrimaryInfohash())
|
||||
|
||||
Reference in New Issue
Block a user