По итогам аудита соответствия спек и кода (11 сабагентов, по одному на capability): Беклог: - новый пункт: гейт авто-раскладки по confidence (спека говорит «вспомогательный сигнал», код делает жёсткий AutoThreshold=0.85) — определиться, что правда - новый пункт: привязка внешних субтитров к серии (спека требует, для сериала связь субтитр→эпизод и пары .idx/.sub в коде не выражены) - новый пункт: раздачи-копии диска DVD/BluRay (VIDEO_TS/BDMV — каталог целиком, не пофайловый разбор) - дополнен существующий баг TVDB /series/: спека metadata-match теперь тоже кодифицирует баг — фикс должен править и требование Спеки (правка на точность, поведение не меняется): - download-tracking, review: «per-download блокировка» → «единая блокировка воркера» (по факту глобальный w.mu, а не per-download) openspec validate --strict — проходит. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 KiB
Беклог
Единый список будущих задач по проекту: то, что уже решили сделать, и
идеи, которые ещё надо обдумать. Это не план реализации (он — в
drafts/roadmap.md) и не источник истины: принятое и
реализованное переезжает в docs/specs/docs/adr.
Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку. Спекулятивные пункты (ещё без решения «делаем») помечены (идея) — их сперва надо проработать.
Высокий
Проблема второго сезона
Если первый сезон сериала уже разложен, а мы добавляем второй/третий/…,
новый сезон должен лечь в ту же папку сериала, а не завести рядом почти
одинаковую вторую. Разбор (drafts/logical-title-model.md)
показал: проблема не в группировке, а в сходимости папки — папка каждый
раз печатается заново из выхода LLM, и совпадение provider_id не
гарантирует совпадение строки («Fargo» vs «Фарго», год сезона vs год
сериала). Отдельная сущность «тайтл» не вводится; решение — правило
сходимости при построении плана: при подтверждённом матче наследовать базу
папки от живых file_link'ов загрузок с тем же (provider, provider_id),
игнорируя LLM-выход; якоря нет — папка из распознавания, как сейчас (первая
загрузка «печатает» имя).
- lookup живых ссылок по
(provider, provider_id)через current recognition - наследование базы папки (имя + год) при построении плана раскладки
- рассинхрон (несколько живых папок с одним матчем) → review, не молча
- тесты: сходимость, отсутствие якоря (свежая папка), смена провайдера
Связано: drafts/logical-title-model.md §5.2, recognition.md (модель уверенности, матч в базе), jellyfin-layout.md (папка сериала с провайдер-id).
Раздачи с докачиванием (слияние при повторном добавлении)
Свежий сериал раздают по мере выхода: торрент содержит 5 эпизодов из 10, позже его перезаливают целиком, и пользователь добавляет раздачу повторно. Решение проработано (drafts/logical-title-model.md §6.2): новая загрузка приходит в ту же папку за счёт правила сходимости, а раскладка становится merge — доложить только недостающее. Существующие пути не трогаем (never-overwrite, владение остаётся у старой загрузки), новые кладём (владеет новая). Split-ownership сезона (серии поделены между загрузками) принят как норма per-path модели; обе раздачи сидируют независимо.
- в плане раскладки отличать «путь занят живой ссылкой того же матча» (→ пропустить) от настоящей коллизии (→ review, как сейчас)
- merge-раскладка: существующее пропустить, недостающее доложить
- показать итог в карточке: сколько доложено, сколько уже было
- решить «слияние загрузок» при перезаливе той же вещи (одна строка
download+ новый infohash vs новая загрузка) — открытый вопрос черновика §10
Зависит от правила сходимости («Проблема второго сезона») и выигрывает от ULID-идентичности.
Связано: jellyfin-layout.md (раскладка, идемпотентность), workflow.md (повторный прогон загрузки).
Удаление средствами jellybit («единое окно», path 2)
Распознавание ручного удаления (источник из qBittorrent / цель из
Jellyfin) и пометка рассинхрона уже сделаны: фоновая сверка по матрице
«источник × цель» → состояния target_missing/orphaned/deleted,
безопасный undo (не снимает последнюю копию, nlink <= 1), синхронный
preflight перед действиями. См. openspec/specs/state-reconciliation/,
workflow.md → «Сверка с реальностью».
Осталось (path 2) — продолжение «единого окна»: удалять просмотренное
из самого jellybit, не идя руками в qBittorrent/Jellyfin. Решения из
разбора (drafts/logical-title-model.md
§5.3, §6.4): «тайтл» — вычисляемая группа загрузок по
(provider, provider_id) / общей папке, без новой сущности; удаление
целиком — обход загрузок группы штатным undo; удаление раздачи из
qBittorrent — осознанный выход за инвариант «источник неприкосновенен»,
только по явному подтверждению (не случайному клику).
- удаление одной загрузки: снять её живые хардлинки (штатный undo,
supersededпропускаем,nlink-гард) + опц. удалить раздачу из qBittorrent с файлами — с осознанным подтверждением - вычисляемая группа «тайтл» в UI: состав сериала/фильма (загрузки, сезоны, файлы) одним экраном
- удаление тайтла целиком: обход загрузок группы + опц. снос опустевшей папки
- после полного удаления память о тайтле не остаётся (линза без содержимого не нужна)
Связано: drafts/logical-title-model.md, ADR-2026-06-13-hardlinks, architecture.md → «Раскладка файлов», workflow.md.
Ретеншн и очистка БД
Терминальные задачи (done/cancelled/failed/reverted), их попытки
recognition с сырыми ответами LLM и metadata_candidate копятся вечно —
со временем БД и список загрузок распухают и становятся нечитаемыми. Нужна
авточистка старше N дней (с настройкой в [storage] или [worker]) и/или
ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере
эксплуатации.
Связано: architecture.md → «Хранилище» (таблицы
download/recognition/metadata_candidate/file_link), пакет store.
Eval-харнес распознавания (корпус кейсов + метрика точности)
Распознавание — ядро продукта, но смена модели или правка промпта сейчас
вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские
релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по
нему с метрикой точности (тип/название/год/нумерация). Тогда можно
сравнивать LLM-провайдеры и версии промпта по числам, а не на ощупь.
Прогон — отдельной командой (jellybit eval или тестом), на фикстурах, без
реального qBittorrent.
Связано: recognition.md (конвейер, модель
уверенности), пакет recognize.
НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)
Сейчас потолок по нагрузке нигде не зафиксирован: воркер, поллинг qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз». Записать в нефункциональные требования целевой ориентир — архитектура держит до 100 одновременных загрузок в работе (приём → распознавание → раскладка), план-максимум — 1000. Сама запись требования дешева и высокоценна: она задаёт рамку для решений ниже по списку. Отдельно (уже дороже) — аудит узких мест под эту цифру: одиночное соединение SQLite и сериализация записи, конкурентность воркера и лимит параллельных распознаваний, частота/стоимость поллинга и дедуп при наплыве.
Связано: architecture.md → «Отслеживание
загрузки»/«Хранилище», пакеты worker, store, qbt, llm.
Средний
Словарь единого языка (ubiquitous language)
Свести термины домена в один глоссарий, чтобы пользователь, документация, код и агент говорили на одном языке: загрузка, раздача, распознавание, матч, кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и т.д. — русский термин, английский идентификатор в коде, краткое определение. Сейчас наименования расходятся между спеками, UI и кодом, и в диалоге с агентом приходится каждый раз сверять понятия. Глоссарий — источник истины по именам; на нём же строится агент-ревьювер наименований (см. «Агенты-ревьюверы качества»).
Связано: docs/conventions (кросс-каттинг), architecture.md (домен), новый файл-глоссарий.
Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль)
Набор узких сабагентов-ревьюверов поверх ревью-процесса из CLAUDE.md,
каждый со своей оптикой: соответствие наименований словарю единого языка,
соблюдение архитектурных границ (единое ядро/тонкие транспорты, инварианты
безопасности данных), конвенций (ошибки, логирование, конфиг, TZ), стиля
кода на высоком уровне и поиск дублирования. Запускаются как чекпоинт перед
archive/коммитом. Развивает ревью-процесс OpenSpec в сторону
воспроизводимых автоматических проверок, не заменяя человеческое ревью.
Связано: CLAUDE.md (ревью-процесс, конвенции),
docs/conventions,
«Словарь единого языка».
Сила совпадения кандидата и пересмотр распознавания/матчинга (идея)
Сейчас у кандидата метабазы нет метрики силы совпадения (metadata_candidate
хранит provider/id/title/year/url), а решение «авто vs review» — по правилу
«единственный сильный матч + валидация», не по числовой уверенности. Для
ревью это значит: список кандидатов нечем отсортировать/подсветить по
уверенности — берём порядок сбора. Идея — ввести на этапе матча силу
совпадения кандидата (точное совпадение названия+года vs частичное) для
сортировки и подсказки в UI. Шире — отдельно продумать сам процесс
распознавания и матчинга: границы «разбор LLM / поиск в базе / сверка»,
что храним у кандидата, как считаем и показываем уверенность. Требует
проработки перед реализацией.
Связано: recognition.md (модель уверенности), ADR-2026-06-13-auto-link-requires-db-match, review-ux.md (выбор источника в ревью, реализовано), «Пересмотр набора capabilities».
История переходов загрузки
Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто инициировал — воркер, человек, сверка), а не только текущее состояние. Сейчас по задаче виден лишь актуальный статус, а разбор «как мы сюда попали» идёт по логам сервера. Отдельная таблица истории даёт лог переходов в карточке/расширенной информации и фундамент для метрик длительности стадий. Естественно ложится на собственный идентификатор загрузки.
Связано: детальный экран загрузки (/download/{id}) уже реализован — лог
переходов ложится в него;
drafts/logical-title-model.md §5.4 (схема
state_transition, actor worker|human|reconcile),
workflow.md (граф состояний),
database.md, пакеты worker, store.
Машина состояний на go-библиотеке
Сейчас FSM реализована вручную в worker. Выбрать подходящую go-библиотеку
для описания воркфлоу/машины состояний и перевести переходы на неё — ради
декларативности, проверяемости переходов и единого места правды. Кандидаты
для оценки: looplab/fsm, qmuntal/stateless (и аналоги). Граф и переходы
уже формализованы — переносим один в один.
Связано: workflow.md (текущий граф состояний).
Привязка уведомлений к источнику в ботах (мульти-бот)
Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней. Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся единым для всех и точкой правды по функциональности (боты — тонкие адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и идентификатор отправителя, маршрутизировать пинги по нему.
Связано: review-ux.md (разделение труда транспортов, веб = точные правки), architecture.md → «Транспорты».
Улучшения UI: показывать матч с записью метабазы
Web-сторона реализована: страница загрузки /download/{id} и экран ревью
показывают, с какой именно записью метабазы (TMDB/TVDB/IMDb) сматчилась
загрузка — провайдер, id и ссылку на запись. Осталось довести то же в
Telegram: в уведомлениях/подтверждениях показывать запись матча (название,
год, провайдер-id, ссылку), чтобы ошибочную привязку было видно и из бота.
Полный выбор источника в вебе уже реализован — см.
review-ux.md.
Связано: review-ux.md, recognition.md (матч в базе), architecture.md → «Транспорты».
Аниме с абсолютной нумерацией
Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а
Jellyfin ждёт SxxEyy. Нужен пересчёт абсолютной нумерации в сезон/серию —
надёжнее всего через TVDB (там есть absolute order). Отдельный крайний
случай распознавания; на стороне ревью — веб-хелпер «absolute → S·E».
Связано: recognition.md (конвейер, сезон-паки), jellyfin-layout.md (нумерация серий), review-ux.md (крайние сценарии).
Добавление торрентов файлом/ссылкой — «единое окно»
Поддержать источники помимо magnet: .torrent-файл и URL (отдаём их в
qBittorrent, без исходящих запросов на пользовательский URL — SSRF
исключён). Идеал — одно поле «единого окна»: кидаем туда текст или файл, а
сервис сам разбирает, что это (magnet / ссылка / .torrent / сообщение
бота), и заводит загрузку.
Связано: architecture.md → «Транспорты»
(source_type = magnet|torrent|url уже в схеме), пакет ingest (сейчас
поддержан только magnet).
Бэкап SQLite
architecture.md требует «бекапить data-том», но как — не описано. Без
понятной стратегии сбой или редеплой стирают всё in-flight состояние.
Зафиксировать решение и реализовать: периодический VACUUM INTO в
/data/backups по расписанию (с ротацией) либо потоковая репликация
(litestream). Лучше сделать, пока БД маленькая.
Связано: architecture.md → «Деплой» (data-том,
«бекапить-и-не-терять»), пакет store.
Глубокий healthcheck и статус зависимостей
/healthz проверяет только сам сервис. Если qBittorrent, LLM или метабаза
недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка
ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent
недоступен»), чтобы причина простоя была видна сразу.
Связано: architecture.md → «Деплой» (healthcheck),
пакеты qbt, llm, metadata, httpapi.
Обучение на правках человека (few-shot из прошлых ревью)
Когда человек поправил матч, тип или нумерацию — сохранять это как пример и
подмешивать похожие в будущие промпты. Системно повышает точность на «твоих»
трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой
верификации, но дешевле: учимся на уже собранных hint/override.
Связано: recognition.md (конвейер, промпт),
«Многоступенчатая верификация»,
architecture.md → «Хранилище» (hint, override).
Гейт авто-раскладки по confidence: спека vs код
Аудит спек↔код (2026-07-03) нашёл расхождение в модели уверенности.
Спека recognition (унаследовано из recognition.md) утверждает, что
самооценка LLM confidence — вспомогательный сигнал, НЕ единственный гейт:
при подтверждённом матче в базе + чистой структурной валидации + согласованности
сигналов авто-раскладка допускается. Код же (internal/recognize/validate.go,
confidence < AutoThreshold, дефолт 0.85) делает confidence жёстким
блокирующим условием: план с матчем и чистой валидацией, но confidence 0.5
уйдёт в review вопреки сценарию спеки. Нужно определиться, что правда: либо
признать порог AutoThreshold в спеке как легитимный гейт (скорее так — код его
осознанно ввёл конфигом), либо ослабить код. Заодно AutoThreshold как
конфигурируемый гейт спекой не описан.
Связано: openspec/specs/recognition (требование «Модель уверенности и решение
auto/review»),
ADR-2026-06-13-auto-link-requires-db-match,
пакет recognize.
Привязка внешних субтитров к серии (сериалы)
Аудит спек↔код (2026-07-03): спека recognition требует «внешние субтитры SHALL
привязываться к соответствующему видео». Для фильма это работает — раскладка
именует субтитр по базе видеофайла. Для сериала связь субтитр→конкретная
серия не выражена: в PlanFile (internal/recognize) нет поля привязки, и нет
логики спаривания VobSub .idx+.sub. Нужно смоделировать привязку субтитра к
эпизоду (поле на PlanFile или роль с указанием season/episode) и спаривание
.idx+.sub, либо — если поддержку откладываем — сузить формулировку спеки до
реального поведения.
Связано: openspec/specs/recognition (требование «Роли файлов на краях»),
jellyfin-layout.md (имена субтитров), пакеты
recognize, layout.
Раздачи-копии диска (DVD/BluRay: VIDEO_TS/BDMV)
Иногда для очень редких фильмов скачивается не один видеофайл, а полная копия
диска — структура VIDEO_TS/ (DVD: VIDEO_TS.IFO, VTS_01_1.VOB…) или
BDMV/ (BluRay: BDMV/STREAM/*.m2ts, index.bdmv). Сейчас распознавание и
раскладка заточены под пофайловый разбор (один main-видеофайл фильма / серии
сериала), а тут «фильм» — это каталог целиком. Jellyfin такие раскладки
поддерживает (папка фильма с вложенным VIDEO_TS/BDMV), нам нужно: распознать,
что раздача — это образ диска (по наличию VIDEO_TS/BDMV), не пытаться
разбирать её по отдельным VOB/m2ts как серии, и разложить весь каталог диска
хардлинками в папку фильма Jellyfin (Название (Год)/VIDEO_TS/…). Крайний, но
реальный случай для редких изданий; частота низкая, поэтому в «Среднем».
Связано: recognition.md (роли файлов, что игнорируем),
jellyfin-layout.md (раскладка фильма, крайние
случаи), пакеты recognize, layout.
Низкий
Баг: ссылка на запись TVDB всегда /series/ (для фильмов ведёт не туда)
metadata.TVDB.Search знает тип запроса (series/movie,
tvdb.go строки 156–159 — он идёт в API), но
URL кандидата хардкодит .../dereferrer/series/{id} (там же, строка 179)
независимо от типа. Для фильма ссылка ведёт на /series/{id} вместо
/movie/{id}, хотя идентификатор верный. Пример: «Последний единорог», id
3015 → отдаём https://www.thetvdb.com/dereferrer/series/3015, рабочая —
https://www.thetvdb.com/dereferrer/movie/3015. matchURL/sourceMatchURL
(internal/httpapi/review.go) предпочитают сохранённый URL кандидата над
providerURL (который тип учитывает верно), поэтому неверный /series/
виден в UI. Фикс — подставлять тип запроса в URL (одна строка); заодно
свериться, что providerURL не расходится по типам.
Аудит спек↔код (2026-07-03) подтвердил баг и выявил, что спека
metadata-matchтеперь тоже «узаконивает» его: требование «Кандидат несёт URL» задаёт для TVDB единственный формат/dereferrer/series/{id}без различения типа. Фикс должен править и код, и это требование (movie-вариант по аналогии с TMDB).
Связано: recognition.md (сверка с базой, кандидаты),
openspec/specs/metadata-match (требование «Кандидат несёт URL»), пакеты
metadata, httpapi.
Мгновенные обновления через SSE
Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает, но с задержкой в интервал опроса и холостыми запросами. Перевести динамический контент (прогресс загрузки, смена статуса, раздача) на Server-Sent Events, чтобы обновления приходили почти мгновенно и без лишнего поллинга. Поллинг работает, поэтому это улучшение, а не блокер; SSE — один долгоживущий ответ на соединение, ложится на server-rendered UI без тяжёлого фронтенда.
Связано: architecture.md → «Транспорты»,
review-ux.md, пакет httpapi.
Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)
По калибровке болей (2026-07-02, drafts/logical-title-model.md §6.3) — не боль, из приоритета выпало. Сосуществование версий доступно уже сейчас (Jellyfin multi-version, другой целевой путь), коллизия на тот же путь штатно уходит в review. Явный replace (undo старого хардлинка → lay нового → супересид владения путём) — отдельный change, если/когда станет болью.
Связано: «Раздачи с докачиванием», jellyfin-layout.md (never-overwrite, коллизия).
Многоступенчатая верификация привязки (идея)
Несколько раз извлекать данные из раздачи и контекста разными промптами, искать в метабазах, затем сводить результаты в общий вердикт (голосование/консенсус) — выше точность ценой нескольких вызовов LLM и запросов к базам. Требует проработки: когда включать, как мерджить расхождения, стоимость/латентность.
Связано: recognition.md (конвейер и модель уверенности).
Выбор из нескольких находок метабазы в Telegram
Когда распознавание даёт несколько подходящих кандидатов в метабазе, предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча брать первый/лучший. Веб остаётся точкой точных правок (полный выбор источника уже реализован — см. review-ux.md), бот — быстрый выбор из готового короткого списка.
Связано: review-ux.md (боты — быстрые действия, веб — точные правки), recognition.md (кандидаты матча).
Проверка свободного места перед copy-fallback
Когда хардлинк невозможен (EXDEV/ENOTSUP/…), layout копирует файл,
дублируя место на диске. На забитом диске это упрётся в полку посреди
раскладки. Перед копированием проверять доступное место и при нехватке
внятно уходить в failed с понятной причиной, а не падать на полпути.
Связано: architecture.md → «Раскладка файлов»
(фолбэк-копирование), пакет layout.
Кэш метабаз (и опционально LLM)
Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее полезен, т.к. вход меняется подсказками).
Связано: recognition.md (сверка с базой), пакеты
metadata, llm.
guessit как сервис-спутник (идея)
go-ptn слабее питоновского guessit. Если точности пред-парса не
хватит — завернуть guessit в крошечный HTTP-сервис (один файл,
поставляется рядом с бинарём jellybit) и спрашивать его на шаге
пред-парса. Сохраняет «доставку копированием»: два файла вместо одного.
Связано: recognition.md → «На будущее» (пред-парс).
Завершение загрузки через webhook (идея)
Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд. Альтернатива: «Run external program on torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом qBittorrent. Решим по опыту эксплуатации.
Связано: architecture.md → «Отслеживание загрузки»,
пакет worker.
Авторизация веб-UI (на будущее)
Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей
(http.trusted_subnets) — как умеет qBittorrent. Если понадобится защита:
токен/Basic в самом приложении или вынос за reverse-proxy с
аутентификацией.
Связано: architecture.md → «Транспорты» (доступ к
веб-UI), пакет httpapi.
Современный Web-UI как PWA
Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое, удобное с телефона). Текущий server-rendered UI функционален, поэтому это улучшение, а не блокер; большой объём работы.
Связано: review-ux.md (веб = точные правки),
пакет httpapi.