ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add

- имя раздачи нормализуется на границе разбора: вырожденное `-`
  (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше
  не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки
  вывода имени
- контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и
  удерживается структурно; три транспорта перестали обещать идентификатор,
  которого нет, и коррелируют отказ по request_id
- scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах
  воркера — записи об отказе qBittorrent и метабаз получили download_id
  и infohash; граница разбора bencode записана в docs/research
This commit is contained in:
av
2026-08-06 18:20:12 +03:00
parent 52615e4e49
commit d081ef1d30
30 changed files with 2190 additions and 63 deletions
+33
View File
@@ -56,12 +56,45 @@ URL `/download/{id}`, параметры форм и команд. Синтак
глобальной уникальности ULID поиск по значению id (grep/jq) SHALL находить
все записи журнала, относящиеся к сущности, независимо от имени поля.
Scoped-логгер SHALL передаваться через `context`, а не доклеиваться к каждой
записи руками. Отсюда обязанность вызывающего, и она ограничена наблюдаемым
исходом: **операция, работающая в контексте загрузки и делающая вызов внешнего
сервиса, SHALL положить scoped-логгер этой загрузки в `context` до такого
вызова** — включая команды, пришедшие с транспорта, а не только фоновый цикл
воркера. Однородность формы у команд, внешних вызовов не делающих, это
требование не нормирует: она принадлежит конвенциям кода.
Причина в том, что клиент внешнего сервиса своей доменной сущности не знает и
знать SHALL NOT — он берёт логгер из `context`. Поэтому вызов внешнего сервиса в
контексте загрузки SHALL давать запись с `download_id` и, когда он известен,
`infohash`; добавлять клиенту поля-дубликаты доменных идентификаторов ради этого
SHALL NOT — источник корреляции один.
Перечень клиентов, ведущих записи о внешних вызовах, живёт в
`docs/conventions/logging.md` и здесь не дублируется. Telegram-клиент таких
записей не ведёт, и уведомление отправляется вне контекста загрузки намеренно
(иначе оно умирало бы вместе с тиком) — это требование его не касается.
Отдельно это важно там, где внешний сервис не сообщает причину отказа: ответ
qBittorrent `Fails.` на добавление раздачи причины не несёт, и единственное, что
делает такую запись пригодной для разбора, — корреляция с загрузкой.
#### Scenario: Путь загрузки по логам
- **GIVEN** загрузка прошла приём, распознавание и раскладку
- **WHEN** журнал фильтруется по значению её `id`
- **THEN** находятся записи всех этапов (ingest, recognition, file-layout)
#### Scenario: Неуспешное добавление в qBittorrent с пути retry
- **GIVEN** загрузка в `failed` с известным инфохэшем, для которой оператор
запросил retry
- **WHEN** qBittorrent отвечает на добавление отказом (`Fails.` либо не-200)
- **THEN** запись о вызове внешнего сервиса содержит `download_id` и `infohash`
загрузки
- **AND** запись находится тем же фильтром по значению id, что и записи
фонового пути добавления
### Requirement: Миграция существующих записей
Существующие записи SHALL получить ULID-идентификаторы одной миграцией с