Files
jellybit/openspec/changes/archive/2026-08-06-ingest-nits/specs/ingest/spec.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

10 KiB
Raw Blame History

ADDED Requirements

Requirement: Вырожденное имя раздачи не считается именем

Система SHALL нормализовать имя раздачи из .torrent на границе разбора источника (internal/torrent) и не считать содержательным именем вырожденное значение - — тот же литерал, которым экосистема обозначает «имени нет» (в библиотеке разбора он выставлен константой metainfo.NoName и присваивается при авторинге раздачи из вырожденного пути). Раздача объявляет это значение сама, полем name; библиотека его не синтезирует, поэтому речь о разборе объявленного значения, а не о фильтре чужого сентинела.

Раздача, у которой поля name нет вовсе, уже даёт пустое имя — это отдельный случай, и он нормализации не требует. Оба случая после разбора SHALL быть неразличимы: имени нет.

Нормализация SHALL применяться к имени раздачи и охватывать три поля, которые из него выводятся:

  • строка названия в синтезированном контексте распознавания (см. «Приём источника из .torrent-файла») — за неимением имени строка названия отсутствует;
  • source_ref загрузки — работает прежний фолбек на имя присланного файла;
  • подсказка вывода отображаемого имени, которую воркер берёт из метаданных торрента (см. download-tracking «Добавление пойманной загрузки в qBittorrent») — подсказка остаётся пустой.

Пути файлов раздачи нормализации не подлежат, и это осознанно. Вырожденное имя попасть в путь файла может — при пустом наборе сегментов путь берётся фолбеком из имени раздачи, ненормализованного. Допустимо потому, что путь файла читается только как число файлов и набор расширений (сигнал по дереву), а целевые пути раскладки строятся не из него, а из ответа qBittorrent (см. file-layout). Появится потребитель, читающий путь как путь, — нормализовать его надо там же, на границе разбора.

Имя раздачи — недоверенный вход, а контекст распознавания читается построчно. Поэтому нормализация SHALL схлопывать в один пробел любые последовательности пробельных символов — включая разделители строк — и убирать краевые, чтобы имя не могло добавить в контекст строку, выглядящую как синтезированный нами факт. Схлопывание SHALL выполняться до сравнения с вырожденным значением, иначе имя вида " - " сравнение не пройдёт, а после схлопывания станет ровно вырожденным и уедет вниз по потоку. Тем же правилом уже обрабатывается комментарий торрента. Защитой от инъекции в промпт оно не является и таковой объявляться SHALL NOT: пользовательский текст контекста многострочен по замыслу и идёт в промпт как есть (см. «Синтез контекста распознавания из полей magnet»).

Правило не однородно с фильтром заглушек-идентификаторов dn у magnet: там заглушка вида *-topic-<id> отбрасывается только в контексте, а в подсказку вывода имени по-прежнему уходит (действующее требование «Синтез контекста распознавания из полей magnet»). Здесь правило строже, и распространение его на magnet — отдельное изменение, этим требованием оно не заказано.

Scenario: Раздача объявила имя -

  • GIVEN .torrent, у которого поле name равно - либо становится равным - после схлопывания пробельного (например " - ")
  • WHEN система разбирает источник и синтезирует контекст распознавания
  • THEN имя раздачи после разбора пусто, строки названия в контексте нет
  • AND остальные строки-факты (размер, файлы, трекер, комментарий) синтезируются как обычно
  • AND подсказка вывода отображаемого имени пуста

Scenario: Раздача без поля name

  • GIVEN .torrent без поля name
  • WHEN система разбирает источник
  • THEN имя раздачи пусто — так же, как у раздачи, объявившей -

Scenario: Вырожденное имя даёт source_ref из имени файла

  • GIVEN .torrent с именем -, присланный файлом Dune.torrent
  • WHEN вызывается приём
  • THEN source_ref загрузки — Dune.torrent, а не -

Scenario: Имя с переводом строки не добавляет строк в контекст

  • GIVEN .torrent, имя которого содержит перевод строки и текст, похожий на синтезированный факт
  • WHEN система синтезирует контекст распознавания
  • THEN имя занимает ровно одну строку названия
  • AND число строк-фактов в контексте такое же, как у раздачи с обычным именем

Requirement: Результат приёма при ошибке пуст

Приём SHALL возвращать транспорту нулевой результат на любом пути ошибки: идентификатор загрузки, инфохэши, состояние и признак дедупликации в этом случае не публикуются. Причина в том, что быстрый приём не создаёт наблюдаемых последствий раньше, чем становится способен вернуть успех: разбор источника, дедуп-чек и заведение загрузки идут до ответа, а всё, что может отказать после заведения (добавление в qBittorrent, вывод имени), выполняет воркер и на исход приёма не влияет.

Контракт SHALL удерживаться структурно — одним местом обнуления результата при ненулевой ошибке, а не аккуратностью каждой ветки возврата: перечень веток растёт, и именно расхождение перечня с текстом породило исходный дефект.

Транспорты SHALL опираться на этот контракт и SHALL NOT обещать пользователю или журналу идентификатор загрузки, которого нет. Корреляционный ключ публичного отказа приёма зависит от транспорта, и требование называет его поимённо:

  • HTTP API и веб-UIrequest_id запроса (см. docs/conventions/errors.md, «Граница и трансляция»);
  • Telegram — корреляционного ключа у отказа приёма нет, и требование это фиксирует как сегодняшнее состояние, а не как цель: диагностика ищется по записи приёма (capability=ingest, infohash). Заводить Telegram-транспорту собственный идентификатор запроса это требование SHALL NOT.

Scenario: Невалидный источник

  • WHEN приём получает источник, который не разбирается ни как magnet, ни как .torrent
  • THEN возвращается ошибка и нулевой результат (идентификатор загрузки пуст)

Scenario: Сбой хранилища на заведении загрузки

  • WHEN приём получает валидный источник, но хранилище отказывает при дедуп-чеке или заведении загрузки
  • THEN возвращается ошибка и нулевой результат

Scenario: Отказ приёма на HTTP-границе

  • GIVEN приём отказал по любой причине
  • WHEN HTTP-транспорт (REST или веб-форма) отвечает пользователю
  • THEN ответ несёт request_id запроса, а не идентификатор загрузки