Files
jellybit/openspec/changes/archive/2026-08-06-ingest-nits/tasks.md
T
av d081ef1d30 ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add
- имя раздачи нормализуется на границе разбора: вырожденное `-`
  (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше
  не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки
  вывода имени
- контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и
  удерживается структурно; три транспорта перестали обещать идентификатор,
  которого нет, и коррелируют отказ по request_id
- scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах
  воркера — записи об отказе qBittorrent и метабаз получили download_id
  и infohash; граница разбора bencode записана в docs/research
2026-08-06 18:20:12 +03:00

143 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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.11.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` записи
называет тот источник, с которым вызов ушёл» — не покрыто: сегодня значения
совпадают, расхождение возможно лишь у загрузки с несколькими хешами; идёт в
урожай.