## 1. F1 — пер-хеш гард дедуп-дозаписи - [x] 1.1 В `CreateDownloadIfNoActive` (`internal/store/download.go`) заменить безусловный цикл `INSERT OR IGNORE` дедуп-ветки на пер-хеш гард как в `AddInfohashes`: пропускать хеш, которым владеет другая активная задача (`findActiveByInfohash(..., existing.ID)`), внутри уже открытой tx. - [x] 1.2 Тест в `internal/store`: гибридный дедуп на B не крадёт хеш, принадлежащий активной A (инвариант сохранён). ## 2. F6 — апгрейд catched-magnet до torrent - [x] 2.1 Добавить guarded-метод хранилища `UpgradeCatchedMagnetToTorrent(ctx, downloadID, torrentBlob) (bool, error)`: в одной tx гардированным UPDATE `source_type='torrent'` при `source_type='magnet' AND state='catched'`, затем сохранить байты в `download_torrent`; вернуть, был ли апгрейд. - [x] 2.2 В `internal/ingest/ingest.go` свести оба дедуп-пути к `attached()` и вызвать апгрейд, когда входящий источник — torrent с байтами (best-effort: неуспех логируется, приём не валится). - [x] 2.3 Обновить интерфейс `ingest.Store` и `fakeStore` в тестах. - [x] 2.4 Тесты в `internal/store`: апгрейд из `catched`+magnet сохраняет байты и меняет `source_type`; из `downloading`/не-magnet — no-op. Тест в `internal/ingest`: дедуп `.torrent` на catched-magnet вызывает апгрейд. ## 3. Проверки - [x] 3.1 `openspec validate --strict ingest-dedup-integrity` - [x] 3.2 `task test` - [x] 3.3 `task lint`