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

126 lines
10 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.
## 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 и веб-UI** — `request_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` запроса, а не идентификатор загрузки