- имя раздачи нормализуется на границе разбора: вырожденное `-` (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки вывода имени - контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и удерживается структурно; три транспорта перестали обещать идентификатор, которого нет, и коррелируют отказ по request_id - scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах воркера — записи об отказе qBittorrent и метабаз получили download_id и infohash; граница разбора bencode записана в docs/research
143 lines
12 KiB
Markdown
143 lines
12 KiB
Markdown
## 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` записи
|
||
называет тот источник, с которым вызов ушёл» — не покрыто: сегодня значения
|
||
совпадают, расхождение возможно лишь у загрузки с несколькими хешами; идёт в
|
||
урожай.
|