ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add

- имя раздачи нормализуется на границе разбора: вырожденное `-`
  (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше
  не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки
  вывода имени
- контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и
  удерживается структурно; три транспорта перестали обещать идентификатор,
  которого нет, и коррелируют отказ по request_id
- scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах
  воркера — записи об отказе qBittorrent и метабаз получили download_id
  и infohash; граница разбора bencode записана в docs/research
This commit is contained in:
av
2026-08-06 18:20:12 +03:00
parent 52615e4e49
commit d081ef1d30
30 changed files with 2190 additions and 63 deletions
+34 -4
View File
@@ -33,7 +33,7 @@ type File struct {
type Info struct {
Infohash string // первичный хеш: v1 приоритетно (нижний hex, 40 для v1, 64 для v2)
Infohashes []string // все хеши файла (гибрид несёт v1 и v2); v1 раньше v2
DisplayName string // имя раздачи (info.name)
DisplayName string // имя раздачи (info.name), нормализованное displayName; пусто, если имени нет
Files []File // файлы раздачи (для одиночного — один элемент)
TotalLength int64 // суммарный размер
Trackers []string // announce + announce-list (плоско, без дублей)
@@ -69,7 +69,7 @@ func Parse(data []byte) (Info, error) {
return Info{
Infohash: hashes[0],
Infohashes: hashes,
DisplayName: meta.BestName(),
DisplayName: displayName(meta.BestName()),
Files: fs,
TotalLength: total,
Trackers: mi.UpvertedAnnounceList().DistinctValues(),
@@ -77,6 +77,33 @@ func Parse(data []byte) (Info, error) {
}, nil
}
// displayName нормализует имя раздачи на границе разбора, чтобы ниже по потоку
// (строка названия контекста, source_ref приёма, подсказка вывода отображаемого
// имени) вырожденных значений не встречалось.
//
// Пробельное схлопываем тем же oneLine, что и комментарий: имя — недоверенный
// вход, а контекст распознавания читается построчно, и имя с переводом строки
// добавило бы в него строку, выглядящую как синтезированный нами факт. Защитой
// от инъекции в промпт это НЕ является — пользовательский текст контекста
// многострочен по замыслу; правило лишь снимает несогласованность между именем
// и комментарием.
//
// Затем отбрасываем `metainfo.NoName` — раздача объявляет это значение сама,
// полем name, по конвенции «имени нет» (библиотека экспортирует константу
// именно затем, чтобы на неё ссылались). Раздача без поля name даёт пустое имя
// и так.
//
// Порядок здесь существенный, и обратный — дефект: имя `" - "` сравнение с
// сентинелом не проходит, а схлопывание превращает его ровно в сентинел, и тот
// уезжает вниз по потоку всеми тремя путями.
func displayName(name string) string {
name = oneLine(name)
if name == metainfo.NoName {
return ""
}
return name
}
// files собирает файлы раздачи и суммарный размер из info (одиночный файл или
// дерево). Байты .torrent — недоверенный вход: на кривых/вырожденных раздачах
// библиотека (UpvertedFiles/FileTree) способна паниковать (напр. делением на
@@ -111,8 +138,11 @@ var announceLabelRe = regexp.MustCompile(`^(?:bt\d*|www|announce|tracker|open)\.
func (i Info) Context() string {
var lines []string
if name := strings.TrimSpace(i.DisplayName); name != "" {
lines = append(lines, name)
// Имя уже нормализовано разборщиком (displayName): вырожденное значение и
// разделители строк сюда не доходят. Проверка на пустоту остаётся — имени
// у раздачи может не быть вовсе, и тогда строки названия просто нет.
if i.DisplayName != "" {
lines = append(lines, i.DisplayName)
}
if i.TotalLength > 0 {
lines = append(lines, "Размер: "+humanSize(i.TotalLength))