Привёл набор capabilities в OpenSpec к цепочке обработки, чтобы имя capability отвечало одному поведению. Чисто по спекам, код и поведение системы не меняются. Change refactor-capability-boundaries (архивирован): - recognition разделён на recognition (разбор LLM) + metadata-match (сверка с базами) - review выделен из web-ui + мигрирован из docs/specs/review-ux.md - новые capability из docs/specs: file-layout, download-tracking, notifications - identity очищен до инфра-id; приём (инфохэши, дедуп, ядро приёма) — в ingest - уведомление о рассинхроне перенесено из state-reconciliation в notifications - дубль владения путём и безопасного undo оставлен в state-reconciliation Итог: 11 capabilities, openspec validate --strict проходит (+37/−11 требований). Источник истины по мигрированным темам переехал в openspec/specs (шапки в docs). Снят пункт беклога «Пересмотр набора capabilities». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
ingest Specification
Purpose
Приём загрузки — единый use-case для всех транспортов (HTTP, Telegram, CLI):
парс источника (Ф1 — magnet), извлечение инфохэшей (download_infohash),
дедупликация по активной задаче, атомарное заведение download и отдача
источника в qBittorrent, а также вывод человекочитаемого отображаемого имени из
контекста (через LLM или алгоритмический фолбек). Держит инвариант «не более
одной активной загрузки на infohash» (атомарный возврат в активное состояние).
Requirements
Requirement: Отображаемое имя торрента из контекста
При добавлении загрузки в qBittorrent система SHALL выводить из контекста
загрузки человекочитаемое отображаемое имя и передавать его в qBittorrent
(параметр rename API /torrents/add), чтобы задача в списке qBit не
показывалась безликим dn magnet-ссылки. Это же имя система SHALL сохранять
у загрузки (download.display_name) для последующего показа заголовком в
веб-UI.
Имя SHALL быть коротким читаемым ярлыком (название, опционально режиссёр и год; для сериала — номер сезона, если он определён), а не куском сырого контекста. Имя SHALL очищаться от управляющих символов и переводов строк и SHALL обрезаться по ограничению длины.
Вывод имени SHALL выполняться синхронно перед отдачей источника в
qBittorrent (параметр rename действует только в момент добавления).
Отображаемое имя SHALL влиять только на отображение (в qBittorrent и как заголовок в веб-UI) и SHALL NOT влиять на пути файлов на диске, распознавание или раскладку — реальные пути система по-прежнему читает из qBit API.
Scenario: Имя из контекста передаётся в qBittorrent
- WHEN загрузку добавляют с непустым контекстом, из которого удалось получить имя
- THEN система передаёт это имя в qBittorrent в параметре
rename - AND имя — короткий читаемый ярлык вида «название (режиссёр, год)», где режиссёр и год опциональны
Scenario: Имя сохраняется у загрузки
- WHEN при приёме получено непустое отображаемое имя
- THEN система сохраняет его в
download.display_nameвместе с созданием загрузки - AND веб-UI использует его заголовком карточки и страницы загрузки
Scenario: Контекст пуст или имя не получено
- WHEN контекста нет либо ни один способ вывода не дал непустого имени
- THEN система добавляет загрузку без параметра
rename - AND qBittorrent оставляет собственное имя (из
dn/торрента) - AND
download.display_nameостаётся пустым, а веб-UI берёт заголовок из фолбека (распознанное название или усечённый источник)
Requirement: Вывод имени через LLM со структурированным выводом
Система SHALL строить отображаемое имя с помощью LLM (структурированный JSON-вывод), извлекая из контекста тип (movie/series), название, год, режиссёра и (для сериала) номер сезона. Год и режиссёр — опциональные поля.
Название SHALL быть на русском языке для российского контента и на английском (оригинальном) — для остального.
Система SHALL предпринять ограниченное число попыток получить от LLM валидный
результат (корректный JSON с непустым названием); бюджет попыток —
[llm].max_retries (по умолчанию 3). Транспортные ретраи провайдера LLM
(сетевые сбои, 429, 5xx) в этот счёт не входят.
Недоступность или ошибка LLM SHALL NOT прерывать приём загрузки: система переходит к алгоритмическому фолбеку.
Scenario: LLM возвращает структурированное имя
- WHEN LLM по контексту возвращает валидный JSON с непустым названием
- THEN система формирует отображаемое имя из его полей (название, год, для сериала — сезон)
Scenario: Российский контент — название на русском
- WHEN контент распознан как российский
- THEN в отображаемом имени используется русское название
Scenario: Исчерпан бюджет попыток LLM
- WHEN LLM за отведённые попытки (
[llm].max_retries) не вернул валидный результат либо недоступен - THEN система не прерывает приём и переходит к алгоритмическому фолбеку
Requirement: Алгоритмический фолбек вывода имени без сети
При неудаче LLM система SHALL выводить имя алгоритмически, без сетевых запросов: брать первую содержательную строку контекста (без ссылок, команд бота и UI-мусора), отсекать технические характеристики, очищать и обрезать по длине.
Если и фолбек не дал непустого имени, система SHALL добавить загрузку без
параметра rename.
Scenario: Фолбек извлекает имя из контекста
- WHEN LLM недоступен или исчерпал попытки, а контекст содержательный
- THEN система берёт первую содержательную строку контекста, отсекает технические характеристики и использует результат как отображаемое имя
- AND при этом не делается ни одного сетевого запроса
Scenario: Фолбек тоже пуст
- WHEN ни LLM, ни алгоритмический фолбек не дали непустого имени
- THEN система добавляет загрузку без параметра
rename
Requirement: Приём источника и заведение загрузки
Приём SHALL быть единым use-case, общим для всех транспортов (HTTP, Telegram,
CLI): по источнику (Ф1 — magnet) и текстовому контексту система SHALL извлечь
инфохэши, дедуплицировать по активной задаче, при отсутствии дубля завести
загрузку (download в состоянии downloading + записи download_infohash) и
отдать источник в qBittorrent (категория qbittorrent.category, savepath). Если
добавление в qBittorrent не удалось, система SHALL перевести уже заведённую
загрузку в failed (error_code qbit_add) и уведомить автора. Заведение
загрузки и запись её хешей SHALL выполняться атомарно (см. «Атомарность возврата
загрузки в активное состояние»).
Scenario: Успешный приём magnet
- GIVEN валидная magnet-ссылка и контекст
- WHEN вызывается приём
- THEN создаётся
downloadвdownloadingс записямиdownload_infohash - AND источник отдан в qBittorrent с нашей категорией
Scenario: Падение добавления в qBittorrent
- GIVEN заведённую загрузку не удалось добавить в qBittorrent
- WHEN обрабатывается ошибка добавления
- THEN загрузка переходит в
failedсerror_codeqbit_add - AND автор загрузки уведомляется
Requirement: Множество инфохэшей загрузки
Загрузка SHALL иметь одну или более записей инфохэша (download_infohash:
infohash lowercase hex, kind ∈ v1|v2). При приёме magnet-ссылки
SHALL записываться ВСЕ известные из неё хеши — гибридный magnet несёт и
btih (v1), и btmh (v2); kind определяется по длине hex (40 — v1, 64 —
v2). Когда qBittorrent сообщает для раздачи оба хеша (infohash_v1,
infohash_v2), система SHALL дописывать недостающие записи загрузке;
усечённый хеш v2-only раздачи (поле hash qBittorrent, 40 hex от v2)
записываться SHALL NOT. Сопоставление раздачи qBittorrent с загрузкой
(поллинг, discover) SHALL выполняться по любому из известных хешей. Один и
тот же infohash MAY принадлежать нескольким загрузкам во времени (повторный
приём после терминального состояния), но активной из них MUST быть не более
одной.
Scenario: Гибридный торрент раскрывает оба хеша
- GIVEN загрузка принята по magnet с v1-хешем
- WHEN qBittorrent отдаёт раздачу с заполненными
infohash_v1иinfohash_v2 - THEN у загрузки появляются обе записи (
kind=v1иv2)
Scenario: Сопоставление по v2-хешу
- GIVEN загрузка с записями v1- и v2-хешей
- WHEN поллинг находит раздачу, совпавшую только по v2-хешу
- THEN раздача сопоставляется с этой загрузкой
Requirement: Дедупликация приёма по любому из хешей
При приёме система SHALL искать активную (нетерминальную) загрузку по
любому из известных хешей и, найдя, SHALL возвращать её вместо создания
новой. Проверка активности и вставка новой загрузки с её хешами SHALL
выполняться атомарно (в одной write-транзакции), поддерживая инвариант «не
более одной активной загрузки на infohash». Отдельного снимаемого/
восстанавливаемого ключа идемпотентности в схеме быть SHALL NOT — активность
выводится только из state.
Scenario: Повторный приём при активной загрузке
- GIVEN активная загрузка с infohash
h - WHEN принимается magnet с тем же
h - THEN новая загрузка не создаётся, возвращается существующая
Scenario: Повторный приём после завершения
- GIVEN загрузка с infohash
hв терминальном состоянии (done) - WHEN принимается magnet с тем же
h - THEN создаётся новая загрузка со своим ULID и записью
h
Requirement: Атомарность возврата загрузки в активное состояние
Система SHALL атомарно (в одной write-транзакции) проверять на каждом пути, возвращающем загрузку из терминального состояния в активное (ручной retry, воскрешение фоновой сверкой, повторная раскладка/relink) или создающем её (приём, adopt чужой раздачи), что никакая другая активная загрузка не владеет любым из хешей этой, и при владении SHALL отказывать в переходе, сохраняя инвариант «не более одной активной загрузки на infohash». Отказ SHALL происходить до побочных эффектов во внешних системах (повторного добавления торрента в qBittorrent).
Та же проверка SHALL применяться к дозаписи хешей загрузке (раскрытие гибридного торрента): хеш, которым владеет другая активная загрузка, дописан быть SHALL NOT. Прямой перевод терминальной загрузки в активное состояние в обход этой проверки SHALL отклоняться хранилищем (механический бэкстоп вместо удалённого unique-индекса).
Scenario: Retry при занятом хеше
- GIVEN загрузка #1 в
failedс хешемh, и другая активная загрузка #2 с тем жеh - WHEN пользователь вызывает retry для #1
- THEN переход отклоняется с пояснением, #1 остаётся в
failed - AND активной по
hостаётся #2