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