## 1. Парсинг полей magnet

- [x] 1.1 Расширить `magnet.Info` полями `ExactLength int64` (из `xl`),
  `Sources []string` (из `xs`), `Keywords []string` (из `kt`)
- [x] 1.2 В `Parse` заполнить новые поля из `vals`; `xl` парсить строго как
  десятичное целое (байты), при ошибке — оставлять 0 (best-effort)
- [x] 1.3 Тесты парсинга: magnet с `xl`/`xs`/`kt`, гибридный, невалидный `xl`,
  отсутствие полей
- [x] 1.4 Устойчивость `Parse` к ссылке, скопированной целиком в
  процент-кодировке (`magnet%3A%3F…`): разовый `QueryUnescape` на фолбек-пути +
  тест; тест на реальной рутрекер-ссылке (dn с кириллицей → Context)

## 2. Синтез контекста из полей

- [x] 2.1 Реализовать `func (Info) Context() string` (или `SynthContext`) в
  пакете `magnet`: строки-факты через `\n` — название (`dn`, если не заглушка),
  `Размер:` из `xl` (человекочитаемо), `Трекер:`/происхождение из домена
  `tr`/`xs`, `Ключевые слова:` из `kt`
- [x] 2.2 Детектор заглушки `dn` вида `*-topic-<id>` (не включать как название)
- [x] 2.3 Helper нормализации домена трекера/источника из URL (`tr`/`xs`)
- [x] 2.4 Тесты синтеза: содержательный `dn`; `dn`-заглушка `rutracker-topic-*`;
  только размер; только домен; все поля вместе; пустой `Info` → пустая строка;
  проверить отсутствие сетевых вызовов (чистая функция)

## 3. Слияние в ingest

- [x] 3.1 В `Ingest` собрать `enrichedContext = join(nonEmpty(req.Context,
  info.Context()))` — пользовательский текст первым, синтез следом
- [x] 3.2 Класть `enrichedContext` **только** в `download.Context` (вместо
  сырого `req.Context`); вызов `namer.DeriveName(ctx, req.Context,
  info.DisplayName)` оставить как есть — namer синтез НЕ получает
- [x] 3.3 Тесты ingest: magnet-only (пустой `req.Context`) → `download.Context`
  синтезирован; непустой текст + поля → оба присутствуют, текст первым; пустой
  текст и бедный magnet → пустой контекст, приём проходит
- [x] 3.4 Регресс-тест: голый rutracker-magnet (`dn=rutracker-topic-<id>`,
  только `tr`, без текста) → `download.display_name` НЕ равен `Трекер: …`
  (именование не деградирует), а `download.Context` содержит домен трекера

## 4. Транспорты и проверка

- [x] 4.1 Убедиться, что приём голого magnet (пустой контекст) штатно проходит
  в `httpapi` и `tgbot` (при необходимости — тест на пустой `context`)
- [x] 4.2 `task test` и `task lint` зелёные
- [x] 4.3 `openspec validate magnet-context-extraction --strict` проходит
