ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add
- имя раздачи нормализуется на границе разбора: вырожденное `-` (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки вывода имени - контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и удерживается структурно; три транспорта перестали обещать идентификатор, которого нет, и коррелируют отказ по request_id - scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах воркера — записи об отказе qBittorrent и метабаз получили download_id и infohash; граница разбора bencode записана в docs/research
This commit is contained in:
@@ -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-идентификаторы одной миграцией с
|
||||
|
||||
@@ -645,3 +645,126 @@ SHALL NOT проваливать добавление загрузки, а са
|
||||
- **WHEN** выполняется шаг добавления
|
||||
- **THEN** структура не выводится, `download.parsed_context` пуст
|
||||
|
||||
### 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` запроса, а не идентификатор загрузки
|
||||
|
||||
Reference in New Issue
Block a user