## Context Ingest (`internal/ingest/ingest.go`) сейчас принимает `Request{Source, Context}`, где `Context` — опциональный текст от транспорта (Telegram-парсер чистит пересланное сообщение бота; HTTP-форма отдаёт поле `context`). Из magnet парсер (`internal/magnet/magnet.go`) достаёт только `xt` (хеши), `dn`, `tr`. `req.Context` кладётся в `download.Context` и подаётся в `namer.DeriveName(ctx, req.Context, info.DisplayName)` — `dn` идёт отдельной «подсказкой». Важно: `dn` в `download.Context` **не** попадает, поэтому recognition его сейчас не видит вовсе. `download.Context` имеет двух потребителей: recognition-промпт (`internal/recognize/prompt.go:87`) и веб-UI страницы загрузки (`internal/httpapi/download.go:100`) — синтез станет виден обоим. При приёме голого magnet без текста `download.Context` почти пуст, и recognition теряет дешёвый сигнал (имя релиза, размер, происхождение), который физически лежит в самой ссылке. ## Goals / Non-Goals **Goals:** - Выжать в контекст распознавания максимум из полей magnet (`dn`, `xl`, `tr`/`xs`, `kt`) без сетевых запросов. - Дополнять этим контекстом пользовательский текст (не терять его, ставить первым), а при пустом тексте — синтезировать контекст целиком. - Обогащённый контекст идёт **только в `download.Context`** (recognition + UI). Вход namer и отображаемое имя не меняем. **Non-Goals:** - Дообогащение со страницы трекера (`*-topic-` → HTTP). Отдельная задача. - Изменение схемы БД, API транспортов, сигнатуры `naming.DeriveName`. - Изменение вывода отображаемого имени: строки-факты (`Размер:`, `Трекер:`) не должны становиться `display_name`. - Использование дерева файлов торрента (оно приходит от qBittorrent позже, не из ссылки) — вне этого change. ## Decisions ### Р1. Расширяем `magnet.Info`, синтез контекста — в `internal/magnet` `Info` получает поля `ExactLength int64` (из `xl`), `Sources []string` (из `xs`), `Keywords []string` (из `kt`). `DisplayName`, `Trackers` уже есть. Функцию синтеза человекочитаемого контекста (`func (Info) Context() string` или `SynthContext`) размещаем в пакете `magnet` — она чисто выводится из полей `Info`, легко юнит-тестируется в изоляции и не тянет зависимостей. Ingest лишь склеивает результат с `req.Context`. _Альтернатива:_ синтез внутри ingest. Отвергнуто — раздувает ingest и хуже тестируется; знание формата полей magnet логичнее держать в `magnet`. Значения полей (`dn`, `xs`, `kt`) приходят уже раскодированными: `url.Query()` снимает процент-кодировку — ручной urldecode не нужен. Дополнительно `Parse` устойчив к ссылке, скопированной целиком в процент-кодировке (`magnet%3A%3Fxt%3D…`, например вытащенной из другого URL): при неудаче прямого разбора делаем разовый `QueryUnescape` и пробуем снова. Unescape применяется только на этом фолбек-пути, поэтому значимый `+` в query обычной ссылки не затрагивается. ### Р2. Формат синтезированного контекста — строки-факты Синтез собирает набор коротких строк (по одной на факт) и склеивает через `\n`, в духе того, что уже кладёт Telegram-парсер: ``` Размер: ≈ 2.1 GiB Трекер: rutracker.org Ключевые слова: ``` Это дружелюбно и к LLM (namer/recognition), и к алгоритмическому фолбеку namer, который берёт «первую содержательную строку» — поэтому строка `dn` идёт первой. Точные ярлыки строк уточняются при apply; спека фиксирует состав, не формулировки. ### Р3. Слияние — «дополняет всегда», результат только в `download.Context` Итоговый контекст = `join(nonEmpty(userText, synthFromMagnet))`. Если `userText` пуст — остаётся только синтез; если синтез пуст — только текст; если пусты оба — пустая строка (приём штатно проходит с пустым контекстом, как сейчас). Пользовательский текст идёт **первым** (он содержательнее). Результат кладётся **только** в `download.Context` (его читают recognition и UI). В namer он **не** передаётся — см. Р5. _Альтернатива:_ синтез только при пустом тексте. Отвергнуто пользователем — теряем размер/происхождение, когда текст есть, но беден. ### Р4. Отсев заглушки `dn` `dn` вида `*-topic-` (частый случай рутрекера: `dn=rutracker-topic-6514485`) как строку-название не берём — это не имя, а идентификатор темы. Детектим простым паттерном (`(?i)-topic-\w+$` / `^\w+-topic-`). Остальные поля (размер, домен) синтезируем в любом случае. Домен трекера при этом всё равно сообщит происхождение (`rutracker.org`). ### Р5. Namer НЕ получает синтез — вход именования не меняем `namer.DeriveName(ctx, req.Context, info.DisplayName)` остаётся как сейчас: пользовательский текст + `dn`-подсказка. Синтезированные строки-факты в namer **не** попадают. _Почему:_ фолбек namer (`fallbackName` → `firstMeaningfulLine`) берёт первую содержательную строку контекста как название. Если подать туда синтез, то на самом частом сценарии (голый rutracker-magnet: `dn=*-topic-` — заглушка, `xl`/`kt` нет) первой строкой окажется `Трекер: t-ru.org`, и она станет и `qBit rename`, и `download.display_name` (заголовок карточки в UI) — регресс против нынешнего `rutracker-topic-`. Значимое имя namer и так получает через `dn`-hint, а размер/домен для *имени* бесполезны. Поэтому синтез — только в `download.Context` для recognition; именование не трогаем. _Альтернатива:_ учить `fallbackName` отбрасывать строки `^<Ярлык>:\s`. Отвергнуто — лишняя связанность namer с форматом синтеза; чище просто не подавать синтез в namer. ## Risks / Trade-offs - [Синтетический контекст «зашумляет» вход recognition — размер/домен могут сбить LLM] → Факты подаём короткими помеченными строками (`Размер:`, `Трекер:`), отделимыми от названия; recognition уже толерантен к «грязному» контексту (Telegram-пересылки). Держим синтез лаконичным. - [Синтез виден в веб-UI (страница загрузки рендерит `download.Context`)] → Ожидаемо и полезно (пользователь видит, откуда взят контекст). Держим строки-факты короткими и человекочитаемыми ради этого же. - [Дубль факта: `xl` даёт `Размер:`, а в тексте Telegram размер уже есть] → На практике редко (реальные rutracker-magnet'ы `xl` не несут). Дедупликацию не делаем — recognition толерантен к повтору; помечаем как известный трейд-офф. - [Ложное срабатывание отсева `dn`-заглушки на реальном имени с «-topic-»] → Паттерн якорим на `-topic-<токен>` в начале/конце строки; при сомнении трактуем консервативно (лучше включить лишнее имя, чем потерять). Покрываем тестом на `rutracker-topic-*` и на обычное имя. - [`xl` в разных единицах/формах] → По спецификации magnet `xl` — байты (десятичное целое). Парсим строго; при ошибке поле пропускаем (best-effort, приём не валим). - [Разные транспорты дублируют логику склейки] → Склейку делаем один раз в ingest (общий use-case для всех транспортов), транспорты не трогаем. ## Migration Plan Изменение аддитивное и обратносовместимое: новые поля `Info` опциональны, контекст лишь обогащается. Схема БД не меняется, миграций нет. Откат — ревертом кода; уже созданные `download.Context` остаются валидными. ## Open Questions - Точные ярлыки строк-фактов (`Размер:` vs `size ~`) и локаль размера — решаем при apply, на спеку не влияет. - Стоит ли нормализовать домен трекера (убирать `www.`, `bt.`, `ann`-хосты) — мелочь реализации, вынесем в helper с тестом.