## 1. N1 — вырожденное имя не покидает разборщик - [x] 1.1 В `internal/torrent/torrent.go` нормализовать `DisplayName` в `Parse`: сравнение с `metainfo.NoName` даёт пустую строку; краевые пробелы и разделители строк схлопываются тем же `oneLine`, что уже применяется к комментарию. В доке `Info.DisplayName` сказать, что нормализация здесь и ниже по потоку вырожденное значение не встречается. - [x] 1.2 В `internal/ingest/ingest.go` убрать ветку `|| ref == "-"` из фолбека `source_ref` и поправить комментарий (нормализовано разборщиком). - [x] 1.3 Тесты в `internal/torrent`, три входа врозь: (а) `name` равно `-` → `DisplayName` пуст и `Context()` без строки названия; (б) `name` отсутствует → `DisplayName` пуст (случай уже работал, закрепляем, чтобы не путался с (а)); (в) имя с переводом строки → одна строка названия, число строк-фактов как у обычного имени. Заодно исправить неверный комментарий `TestContextEmptyWhenNoFields` («info без имени (NoName-сентинел)») — он и есть источник исходной ошибки ревью. - [x] 1.4 Тест в `internal/ingest`: `.torrent` с именем `-`, присланный файлом → `source_ref` равен имени файла. - [x] 1.5 Тест в `internal/worker`: для раздачи с именем `-` подсказка, уходящая в `namer`, пуста (третий потребитель `DisplayName` — вход LLM; `fakeNamer` дополнить полем `gotHint`). ## 2. N3 — контракт «на ошибке результат пуст» - [x] 2.1 В `internal/ingest/ingest.go` объявить контракт в доке `Ingest` и `Result` и удержать его **структурно**: именованный возврат + один `defer`, обнуляющий результат при ненулевой ошибке. - [x] 2.2 Тест в `internal/ingest`: на каждом классе отказа (невалидный источник, сбой хранилища на дедуп-чеке, сбой хранилища на заведении) результат равен нулевому значению **целиком**, а не только по полю `DownloadID`. - [x] 2.3 `internal/httpapi/httpapi.go`, REST `handleAPIAdd` — заменить устаревший комментарий и передавать в `s.apiErr` пустой идентификатор. - [x] 2.4 `internal/httpapi/httpapi.go`, веб-форма `handleUIAdd` — то же для `userErr` (третий вызов приёма, в первой редакции задач пропущен). - [x] 2.5 `internal/tgbot/bot.go` — то же для `opErr`; в комментарии назвать честно, что корреляционного ключа у отказа приёма в Telegram нет. ## 3. N4 — корреляция записи о неуспешном добавлении - [x] 3.1 В `internal/worker/worker.go` метод `Retry` присваивает `ctx = w.scoped(ctx, capReview, id, d.PrimaryInfohash())` сразу после чтения загрузки; одноразовые `w.scoped(…)` и ручные `w.log.…` с доклеенными `capability`/`download_id` внутри метода схлопнуть в `logctx.From(ctx)`. - [x] 3.2 Тест в `internal/qbt` с подставным сервером, отвечающим `Fails.`: запись о вызове содержит поля из scoped-логгера `ctx` (`download_id`, `infohash`) и `ext.operation = torrents/add`; **негативная половина** — значение отправленного magnet с узнаваемым `passkey` в записи не встречается ни в одном поле, включая `error`. - [x] 3.3 Тест в `internal/worker`: `Retry`, дошедший до `Add`, вызывает клиента с `ctx`, несущим scoped-логгер загрузки (`fakeQbt` дополнить записью переданного `ctx`). ## 4. N5 — записка о границах разбора `.torrent` - [x] 4.1 Снять замер во временном каталоге (`tmp/`, после замера удаляется): крафт-вход, объявляющий длинную bencode-строку, через `metainfo.Load`; зафиксировать аллокацию и исход для нескольких объявленных длин, включая значение выше потолка. - [x] 4.2 Завести `docs/research/torrent-bencode-limits.md`: версия библиотеки, файл:строка, команда замера, числа, вывод с условиями; наблюдение о `BestName`/`NoName` (что библиотека сентинел не синтезирует); отметить, что panic-гард `files()` не покрывает `Load`/`UnmarshalInfo`/`HashBytes`, и назвать **условие устаревания** — перепроверить при обновлении `anacrolix/torrent`. - [x] 4.3 Добавить строку записки в индекс `docs/research/README.md` и расширить его вводную: каталог держит и наблюдения о поведении наших разборщиков и зависимостей на границе формата. ## 5. Отработка ревью изменения (чекпоинт 2, профиль `deep`) - [x] 5.1 B1 — в `torrent.displayName` схлопывание идёт ДО сравнения с `metainfo.NoName`; тест стал табличным (`-`, `" - "`, `"-\n"`, `"\t-"`, NBSP+`-`); порядок закреплён в дельта-спеке. - [x] 5.2 B2 — scoped-логгер до внешнего вызова добавлен ещё в шести командах (`Relink`, `Rerecognize`, `Refine`, `ChooseCandidate`, `AddManualSource`, `SetProviderID`); ложное «единственная команда» в `proposal.md` исправлено. - [x] 5.3 F1 — тест корреляции по `request_id` на обоих HTTP-транспортах (REST и веб-форма). - [x] 5.4 F2 — инъекция ошибок в `fakeStore` (`SetDownloadState`, `SetRetriedAt`, `SetSourceMissCount`) и тесты best-effort веток `Retry`. - [x] 5.5 F3 — в записку разведки вложен текст программы замера и добавлено разграничение `TotalAlloc`/RSS с числами. - [x] 5.6 F4 — в дельта-спеке исправлены два неточных утверждения (широта схлопывания и обоснование исключения для путей файлов). ## 6. Проверка - [x] 6.1 `task gate` зелёный (go-шаги отработали, не `SKIP`). - [x] 6.2 `openspec validate --strict ingest-nits`. ## Приёмочные критерии ### Из постановки задачи Копия из `docs/tasks/items/ingest-nits.md` — приходят снаружи, пайплайном не сочиняются и не занижаются. - Для безымянного торрента `Context()` не отдаёт «-» как название — поле пустое (оракул: тест разбора на фикстуре безымянного торрента в `internal/torrent`). **Замечание к критерию:** ревью предложения показало, что для торрента *без имени* это верно и до изменения; отдаёт «-» торрент, который сам объявил `name: "-"`. Проверяются оба входа врозь (задача 1.3). - Комментарии в `httpapi` и `tgbot` описывают фактическое поведение `Ingest`: на любом пути ошибки возвращается пустой `Result`, корреляция идёт по `request_id` (оракул: чтение диффа на ревью — механического оракула нет). - Лог неудачного добавления в qBittorrent несёт инфохэш для корреляции (оракул: тест клиента с подставным сервером, проверяющий поля записи). - Наблюдение про аллокации bencode до `MaxStrLen` записано в `docs/research/` с провенансом либо явно отклонено строкой в теле задачи (оракул: `task gate`, шаг канона). ### Из рубрики прохода `review-rubric` Свойства, порождённые до чтения кода; взяты те, что изменение обязано удовлетворить. Непокрытые названы явно. - Нормализация вырожденного имени живёт **ровно в одной точке** — на границе разбора; ни один потребитель `DisplayName` знания о `-` не содержит (оракул: 1.1–1.2 плюс `grep -rn 'NoName\|"-"' internal --glob '!internal/torrent/**'` без попаданий по смыслу «имя раздачи»). - Нормализация имени охватывает разделители строк и краевые пробелы, а не только вырожденное значение (оракул: 1.3в). - У **каждого** из трёх потребителей нормализованного поля свой оракул (оракул: 1.3, 1.4, 1.5 — три теста в трёх пакетах). - Контракт «на ошибке результат нулевой» удерживается структурой, а не перечнем известных путей (оракул: 2.1 — одно место обнуления; 2.2 — сравнение с нулевым значением целиком). - Публичная диагностика отказа приёма несёт корреляционный ключ, и ключ назван поимённо для каждого транспорта (оракул: требование дельта-спеки; для Telegram зафиксировано отсутствие ключа как сегодняшнее состояние — вопрос записан). - Запись об отказе внешнего сервиса самодостаточна и **не несёт секретов** (оракул: 3.2, обе половины). - **Не покрыто и почему:** свойство «обязанность класть scoped-логгер проверена механически перебором всех команд воркера» — перебор потребовал бы правки `Cancel`/`Dismiss`, которую этот change сознательно не делает (открытый вопрос дизайна); оракул остаётся точечным (3.3). Свойство «поле `infohash` записи называет тот источник, с которым вызов ушёл» — не покрыто: сегодня значения совпадают, расхождение возможно лишь у загрузки с несколькими хешами; идёт в урожай.