Жизненный цикл: claim-токен распознавания, индексация хешей, retry сломанного торрента

Три мелких фикса из docs/backlog/review-lifecycle-minor.md (ревью Fable
2026-07-08). MINOR-9 (I/O под глобальным w.mu) осознанно waive для
one-user home-сервера — не трогаем.

MINOR-8: claim-токен распознавания. recognizeOne фиксирует updated_at на
момент claim (перечитывая запись после перехода в recognizing), а
finishRecognition коммитит результат, только если токен совпал. Иначе за
время LLM-вызова задачу увели из recognizing и вернули обратно
(cancel → relink revive) — это уже другой эпизод, устаревший результат
отбрасываем, задача остаётся в recognizing для перезапуска поллингом.

NIT-11: lookup-мапы (byHash/live/torrentByInfohash) больше не индексируют
усечённый 40-hex t.Hash v2-only торрентов. Новый хелпер torrentIndexHashes
зеркалит выбор torrentHashes: t.Hash берём только при отсутствии обоих
infohash_v1/v2. Убирает теоретический ложный матч по коллизии длины.

NIT-12: retry живого, но сломанного торрента (error/missingFiles) теперь
отклоняется с подсказкой починить раздачу (recheck) в qBittorrent, вместо
бессмысленной переотдачи источника (сверка тут же вернула бы задачу в
failed). Повторный Add — только когда раздачи в qBittorrent нет. Меняет
спеку state-reconciliation → дельта openspec/changes/2026-07-17-retry-reject-broken-torrent
(не архивировал).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-17 20:35:14 +03:00
co-authored by Claude Opus 4.8
parent b8017d65eb
commit bcc7b2d76b
10 changed files with 330 additions and 27 deletions
@@ -0,0 +1,42 @@
## Why
Retry задачи в `failed`/`stuck`, чей торрент ЖИВ в qBittorrent, но в состоянии
ошибки (`error`/`missingFiles`), сейчас повторно отдаёт источник. На живом
сломанном торренте это бесполезно: qBittorrent отвергает дубль, задача уходит в
`downloading`, а на ближайшем тике сверки `classErrored` тут же возвращает её в
`failed` (+ дебаунс уведомления). Пользователю retry выглядит сломанным, а
реальное лекарство (перепроверка/`recheck`/восстановление файлов в qBittorrent)
не подсказано (находка ревью NIT-12).
## What Changes
- **Retry сломанного живого торрента отклоняется**, а не переотдаёт источник.
Отказ несёт понятное сообщение: починить раздачу (`recheck`/восстановить
файлы) в qBittorrent и повторить. Состояние задачи не меняется, повторного
`Add` не происходит.
- Повторный `Add` при retry остаётся только для случая, когда раздачи в
qBittorrent НЕТ (перецепка к здоровому живому торренту — без `Add`, как и
раньше).
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `state-reconciliation`: требование «Ручной повтор зависшей/упавшей загрузки из
транспортов» — ветка живого сломанного торрента меняет исход с «повторно отдать
источник» на «отклонить с подсказкой про `recheck`». Прочие ветки retry (нет
раздачи → `Add`; жив и здоров → перецепка без `Add`; сброс базиса `retried_at`)
без изменений.
## Impact
- **Спеки:** дельта `state-reconciliation` (одно MODIFIED-требование).
- **Код:** `internal/worker/worker.go``Retry` (ветка `alive && classErrored`
→ отказ `ErrConflict` с сообщением вместо `reAdd`).
- **Тесты:** `internal/worker/worker_test.go` — отклонение retry на живом
сломанном торренте (`error`/`missingFiles`), контроль перецепки здорового.
- **Миграции БД:** нет.