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

12 KiB
Raw Blame History

1. N1 — вырожденное имя не покидает разборщик

  • 1.1 В internal/torrent/torrent.go нормализовать DisplayName в Parse: сравнение с metainfo.NoName даёт пустую строку; краевые пробелы и разделители строк схлопываются тем же oneLine, что уже применяется к комментарию. В доке Info.DisplayName сказать, что нормализация здесь и ниже по потоку вырожденное значение не встречается.
  • 1.2 В internal/ingest/ingest.go убрать ветку || ref == "-" из фолбека source_ref и поправить комментарий (нормализовано разборщиком).
  • 1.3 Тесты в internal/torrent, три входа врозь: (а) name равно -DisplayName пуст и Context() без строки названия; (б) name отсутствует → DisplayName пуст (случай уже работал, закрепляем, чтобы не путался с (а)); (в) имя с переводом строки → одна строка названия, число строк-фактов как у обычного имени. Заодно исправить неверный комментарий TestContextEmptyWhenNoFields («info без имени (NoName-сентинел)») — он и есть источник исходной ошибки ревью.
  • 1.4 Тест в internal/ingest: .torrent с именем -, присланный файлом → source_ref равен имени файла.
  • 1.5 Тест в internal/worker: для раздачи с именем - подсказка, уходящая в namer, пуста (третий потребитель DisplayName — вход LLM; fakeNamer дополнить полем gotHint).

2. N3 — контракт «на ошибке результат пуст»

  • 2.1 В internal/ingest/ingest.go объявить контракт в доке Ingest и Result и удержать его структурно: именованный возврат + один defer, обнуляющий результат при ненулевой ошибке.
  • 2.2 Тест в internal/ingest: на каждом классе отказа (невалидный источник, сбой хранилища на дедуп-чеке, сбой хранилища на заведении) результат равен нулевому значению целиком, а не только по полю DownloadID.
  • 2.3 internal/httpapi/httpapi.go, REST handleAPIAdd — заменить устаревший комментарий и передавать в s.apiErr пустой идентификатор.
  • 2.4 internal/httpapi/httpapi.go, веб-форма handleUIAdd — то же для userErr (третий вызов приёма, в первой редакции задач пропущен).
  • 2.5 internal/tgbot/bot.go — то же для opErr; в комментарии назвать честно, что корреляционного ключа у отказа приёма в Telegram нет.

3. N4 — корреляция записи о неуспешном добавлении

  • 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).
  • 3.2 Тест в internal/qbt с подставным сервером, отвечающим Fails.: запись о вызове содержит поля из scoped-логгера ctx (download_id, infohash) и ext.operation = torrents/add; негативная половина — значение отправленного magnet с узнаваемым passkey в записи не встречается ни в одном поле, включая error.
  • 3.3 Тест в internal/worker: Retry, дошедший до Add, вызывает клиента с ctx, несущим scoped-логгер загрузки (fakeQbt дополнить записью переданного ctx).

4. N5 — записка о границах разбора .torrent

  • 4.1 Снять замер во временном каталоге (tmp/, после замера удаляется): крафт-вход, объявляющий длинную bencode-строку, через metainfo.Load; зафиксировать аллокацию и исход для нескольких объявленных длин, включая значение выше потолка.
  • 4.2 Завести docs/research/torrent-bencode-limits.md: версия библиотеки, файл:строка, команда замера, числа, вывод с условиями; наблюдение о BestName/NoName (что библиотека сентинел не синтезирует); отметить, что panic-гард files() не покрывает Load/UnmarshalInfo/HashBytes, и назвать условие устаревания — перепроверить при обновлении anacrolix/torrent.
  • 4.3 Добавить строку записки в индекс docs/research/README.md и расширить его вводную: каталог держит и наблюдения о поведении наших разборщиков и зависимостей на границе формата.

5. Отработка ревью изменения (чекпоинт 2, профиль deep)

  • 5.1 B1 — в torrent.displayName схлопывание идёт ДО сравнения с metainfo.NoName; тест стал табличным (-, " - ", "-\n", "\t-", NBSP+-); порядок закреплён в дельта-спеке.
  • 5.2 B2 — scoped-логгер до внешнего вызова добавлен ещё в шести командах (Relink, Rerecognize, Refine, ChooseCandidate, AddManualSource, SetProviderID); ложное «единственная команда» в proposal.md исправлено.
  • 5.3 F1 — тест корреляции по request_id на обоих HTTP-транспортах (REST и веб-форма).
  • 5.4 F2 — инъекция ошибок в fakeStore (SetDownloadState, SetRetriedAt, SetSourceMissCount) и тесты best-effort веток Retry.
  • 5.5 F3 — в записку разведки вложен текст программы замера и добавлено разграничение TotalAlloc/RSS с числами.
  • 5.6 F4 — в дельта-спеке исправлены два неточных утверждения (широта схлопывания и обоснование исключения для путей файлов).

6. Проверка

  • 6.1 task gate зелёный (go-шаги отработали, не SKIP).
  • 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 записи называет тот источник, с которым вызов ушёл» — не покрыто: сегодня значения совпадают, расхождение возможно лишь у загрузки с несколькими хешами; идёт в урожай.