Ingest: контекст распознавания из полей magnet-ссылки

Синтезируем контекст из полей самой magnet-ссылки (dn, xl, tr/xs, kt) без
сети и дополняем им текст от транспорта: пользовательский текст первым, при
пустом — синтез единственный. Обогащённый контекст идёт только в
download.Context (его читают recognition и веб-UI); вход namer и отображаемое
имя не меняются — строки-факты (Размер:/Трекер:) в display_name не текут.

- magnet.Info: поля ExactLength/Sources/Keywords + Info.Context() (синтез,
  отсев dn-заглушек *-topic-<id>, домен трекера, человекочитаемый размер)
- Parse устойчив к ссылке в процент-кодировке (разовый QueryUnescape)
- ingest: mergeContext → download.Context, namer на сыром req.Context
- Влита дельта capability ingest в openspec/specs, change заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-07 20:38:01 +03:00
co-authored by Claude Opus 4.8
parent 6d801ed03b
commit e9ec26d09a
10 changed files with 837 additions and 7 deletions
@@ -0,0 +1,44 @@
## 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` проходит