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
@@ -0,0 +1,78 @@
## Why
Ревью приёма 2026-07-08 оставило четыре нити, каждая из которых по отдельности
дёшева, а вместе они портят три разных наблюдаемых поверхности: контекст
распознавания (грязная строка), комментарии в коде (описывают контракт, которого
уже нет), журнал (запись о неуспешном добавлении без корреляции) и знание о
границе чужой библиотеки (не записано нигде). Пока нити открыты, каждый
следующий проход ревью тратит внимание на то, что уже разобрано.
## What Changes
- **N1 — вырожденное имя не покидает разборщик.** Раздача может объявить полем
`name` значение `-` — конвенция «имени нет» (в библиотеке разбора это
константа `metainfo.NoName`). Сейчас `.torrent`-приём отбрасывает `-` только
для `source_ref`, а `torrent.Info.Context()` и подсказка имени для `namer`
берут его как содержательное название. Нормализация переезжает на границу
разбора: `DisplayName` пустеет прямо в `torrent.Parse`, и три места ниже по
потоку перестают знать про вырожденное значение. Тем же местом схлопываются
разделители строк в имени — иначе имя добавляет в построчный контекст строку,
выглядящую как синтезированный нами факт.
**Основание нити уточнено ревью предложения:** библиотека `-` не синтезирует
(`BestName()` на раздаче без имени возвращает пустую строку), поэтому речь о
разборе объявленного значения, а не о фильтре чужого сентинела; случай
«раздача без поля `name`» уже отрабатывается верно и получает свой сценарий,
чтобы впредь не путаться с первым.
- **N3 — комментарии описывают фактический контракт `Ingest`.** После
fast-catch-рефактора `Ingest` возвращает нулевой `Result` на **каждом** пути
ошибки, а комментарии в `httpapi` и `tgbot` до сих пор обещают непустой
`DownloadID` «при сбое после создания задачи». Контракт закрепляется в
доке `Ingest`, удерживается структурно (обнуление результата одним `defer`, а
не аккуратностью каждой ветки), подтверждается тестом и перестаёт
пересказываться неверно на транспортах. Вызовов приёма три, а не два:
REST, веб-форма и Telegram.
- **N4 — запись о неуспешном добавлении в qBittorrent коррелируется.** Клиент
`qbt` берёт логгер из `ctx` и своего инфохэша не знает (и знать не должен —
разбор источника живёт в `magnet`/`torrent`). Путь `worker.Retry` не кладёт в
`ctx` scoped-логгер загрузки, поэтому запись `Fails.` с этого пути уходит без
`download_id`/`infohash`. Retry приводится к тому же виду, что и `Delete`.
**Ревью изменения показало, что таких команд не одна, а семь** — те же
`Relink`, `Rerecognize`, `Refine`, `ChooseCandidate`, `AddManualSource`,
`SetProviderID` зовут qBittorrent или метабазу до постановки логгера.
Починены все семь: иначе требование `identity` архивировалось бы ложным.
- **N5 — граница чужой библиотеки записывается наблюдением.** `anacrolix/torrent`
аллоцирует объявленную bencode-строку до её чтения, с потолком ~128 MiB.
Правке не подлежит (чужой код), поэтому исход — запись в `docs/research/` с
провенансом и замером, а не изменение кода.
## Capabilities
### New Capabilities
Нет — изменение не вводит нового поведения и новых понятий.
### Modified Capabilities
- `ingest`: приём `.torrent` перестаёт считать вырожденное имя `-`
содержательным — ни в синтезированном контексте, ни в подсказке имени, ни в
`source_ref`; плюс фиксируется контракт «на ошибке приёма результат пуст» и
поимённо называется корреляционный ключ отказа на каждом транспорте.
- `identity`: корреляция в журнале распространяется на записи о вызовах внешних
сервисов, сделанных в контексте загрузки, — включая путь retry.
## Impact
- `internal/torrent` — нормализация `DisplayName` на границе разбора; тест на
безымянной раздаче.
- `internal/ingest` — упрощение фильтра `source_ref`, дока и структурная
гарантия контракта `Result`, тест «на ошибке результат нулевой».
- `internal/httpapi` (REST `handleAPIAdd` **и** веб-форма `handleUIAdd`),
`internal/tgbot` — комментарии и передача пустого id в диагностику вместо
заведомо пустого `res.DownloadID`.
- `internal/worker``Retry` кладёт scoped-логгер загрузки в `ctx`.
- `internal/qbt` — тест с подставным сервером на поля записи о неуспешном `add`.
- `docs/research/` — новая записка про границы разбора `.torrent` (предел
`MaxStrLen` bencode + наблюдение о `BestName`/`NoName`); строка в индексе
`docs/research/README.md` и полстроки в его вводной.
- Схема БД, конфиг, API и тексты Telegram не затрагиваются; нумерованных
артефактов (миграций, ADR) изменение не добавляет.