## 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`
