Files
jellybit/docs/backlog.md
T
avandClaude Opus 4.8 e7fe88a986 Завёл change review-source-selection: выбор источника и предпросмотр в ревью (openspec)
Переработка экрана ревью: единый список источников (нейронка наравне с
кандидатами баз), выбор/переключение/снятие в пользу нейронки, ручное
добавление по id/URL, предпросмотр полей и целевых путей до применения.
Дизайн отревьюен: единая деривация «источник → overrides» (preview==apply,
чинит залипший override title/year). Ограничились существующими capabilities.

Беклог: добавил две идеи — «Пересмотр набора capabilities и рефакторинг спек»
и «Сила совпадения кандидата / пересмотр распознавания и матчинга».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 09:52:48 +03:00

455 lines
35 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.
# Беклог
Единый список будущих задач по проекту: то, что уже решили сделать, и
идеи, которые ещё надо обдумать. Это не план реализации (он — в
[drafts/roadmap.md](drafts/roadmap.md)) и не источник истины: принятое и
реализованное переезжает в `docs/specs`/`docs/adr`.
Приоритет — грубая оценка «ценность / стоимость», не обязательство к
порядку. Спекулятивные пункты (ещё без решения «делаем») помечены
_(идея)_ — их сперва надо проработать.
## Высокий
### Проблема второго сезона
Если первый сезон сериала уже разложен, а мы добавляем второй/третий/…,
новый сезон должен лечь в **ту же** папку сериала, а не завести рядом почти
одинаковую вторую. Разбор ([drafts/logical-title-model.md](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](drafts/logical-title-model.md) §5.2,
[recognition.md](specs/recognition.md) (модель уверенности,
матч в базе), [jellyfin-layout.md](specs/jellyfin-layout.md) (папка
сериала с провайдер-id).
### Раздачи с докачиванием (слияние при повторном добавлении)
Свежий сериал раздают по мере выхода: торрент содержит 5 эпизодов из 10,
позже его перезаливают целиком, и пользователь добавляет раздачу повторно.
Решение проработано ([drafts/logical-title-model.md](drafts/logical-title-model.md)
§6.2): новая загрузка приходит в ту же папку за счёт правила сходимости, а
раскладка становится **merge** — доложить только недостающее. Существующие
пути не трогаем (never-overwrite, владение остаётся у старой загрузки),
новые кладём (владеет новая). Split-ownership сезона (серии поделены между
загрузками) принят как норма per-path модели; обе раздачи сидируют
независимо.
- [ ] в плане раскладки отличать «путь занят живой ссылкой того же матча»
(→ пропустить) от настоящей коллизии (→ review, как сейчас)
- [ ] merge-раскладка: существующее пропустить, недостающее доложить
- [ ] показать итог в карточке: сколько доложено, сколько уже было
- [ ] решить «слияние загрузок» при перезаливе той же вещи (одна строка
`download` + новый infohash vs новая загрузка) — открытый вопрос
черновика §10
Зависит от правила сходимости ([«Проблема второго
сезона»](#проблема-второго-сезона)) и выигрывает от ULID-идентичности.
Связано: [jellyfin-layout.md](specs/jellyfin-layout.md) (раскладка,
идемпотентность), [workflow.md](specs/workflow.md) (повторный прогон
загрузки).
### Удаление средствами jellybit («единое окно», path 2)
Распознавание **ручного** удаления (источник из qBittorrent / цель из
Jellyfin) и пометка рассинхрона уже сделаны: фоновая сверка по матрице
«источник × цель» → состояния `target_missing`/`orphaned`/`deleted`,
безопасный `undo` (не снимает последнюю копию, `nlink <= 1`), синхронный
preflight перед действиями. См. `openspec/specs/state-reconciliation/`,
[workflow.md](specs/workflow.md) → «Сверка с реальностью».
Осталось (path 2) — продолжение «единого окна»: удалять просмотренное
**из самого jellybit**, не идя руками в qBittorrent/Jellyfin. Решения из
разбора ([drafts/logical-title-model.md](drafts/logical-title-model.md)
§5.3, §6.4): «тайтл» — вычисляемая группа загрузок по
`(provider, provider_id)` / общей папке, без новой сущности; удаление
целиком — обход загрузок группы штатным undo; удаление раздачи из
qBittorrent — осознанный выход за инвариант «источник неприкосновенен»,
только по явному подтверждению (не случайному клику).
- [ ] удаление одной загрузки: снять её живые хардлинки (штатный undo,
`superseded` пропускаем, `nlink`-гард) + опц. удалить раздачу из
qBittorrent с файлами — с осознанным подтверждением
- [ ] вычисляемая группа «тайтл» в UI: состав сериала/фильма (загрузки,
сезоны, файлы) одним экраном
- [ ] удаление тайтла целиком: обход загрузок группы + опц. снос
опустевшей папки
- [ ] после полного удаления память о тайтле не остаётся (линза без
содержимого не нужна)
Связано: [drafts/logical-title-model.md](drafts/logical-title-model.md),
[ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md),
[architecture.md](specs/architecture.md) → «Раскладка файлов»,
[workflow.md](specs/workflow.md).
### Ревью: выбор источника совпадения и предпросмотр
Переработать страницу ревью так, чтобы показывать **все** совпавшие
результаты по метабазам списком и дать выбрать из них. Принцип: совпадение
есть **всегда** — мы лишь выбираем источник. Поэтому матч нейронки — это
отдельная строка в том же списке (наравне с кандидатами TMDB/TVDB), а не
особый режим.
Возможности экрана:
- список кандидатов из баз + строка «распознано нейронкой»;
- выбрать один кандидат, переключиться на другой, отменить матч с базой в
пользу нейронки;
- добавить кандидат вручную (по id/url базы), когда автопоиск промахнулся;
- при выборе/переключении — **предпросмотр полей** (название, режиссёр,
год) и **предпросмотр раскладки** (целевые пути) до применения.
Развивает уже реализованный показ матча в вебе (страница загрузки и экран
ревью — провайдер, id, ссылка на запись) и детальный экран `/download/{id}`;
пересекается с быстрым выбором в Telegram. Веб остаётся точкой точных правок.
Связано: [review-ux.md](specs/review-ux.md) (выбор кандидата, «без базы»,
переключатель типа), [recognition.md](specs/recognition.md) (кандидаты
матча, провайдер-id), [«Улучшения UI: показывать матч»](#улучшения-ui-показывать-матч-с-записью-метабазы),
пакет `httpapi`.
### Ретеншн и очистка БД
Терминальные задачи (`done`/`cancelled`/`failed`/`reverted`), их попытки
`recognition` с сырыми ответами LLM и `metadata_candidate` копятся вечно —
со временем БД и список загрузок распухают и становятся нечитаемыми. Нужна
авточистка старше N дней (с настройкой в `[storage]` или `[worker]`) и/или
ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере
эксплуатации.
Связано: [architecture.md](specs/architecture.md) → «Хранилище» (таблицы
`download`/`recognition`/`metadata_candidate`/`file_link`), пакет `store`.
### Eval-харнес распознавания (корпус кейсов + метрика точности)
Распознавание — ядро продукта, но смена модели или правка промпта сейчас
вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские
релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по
нему с метрикой точности (тип/название/год/нумерация). Тогда можно
сравнивать LLM-провайдеры и версии промпта по числам, а не на ощупь.
Прогон — отдельной командой (`jellybit eval` или тестом), на фикстурах, без
реального qBittorrent.
Связано: [recognition.md](specs/recognition.md) (конвейер, модель
уверенности), пакет `recognize`.
### НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)
Сейчас потолок по нагрузке нигде не зафиксирован: воркер, поллинг
qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз».
Записать в **нефункциональные требования** целевой ориентир — архитектура
держит до **100 одновременных загрузок** в работе (приём → распознавание →
раскладка), план-максимум — **1000**. Сама запись требования дешева и
высокоценна: она задаёт рамку для решений ниже по списку. Отдельно (уже
дороже) — аудит узких мест под эту цифру: одиночное соединение SQLite и
сериализация записи, конкурентность воркера и лимит параллельных
распознаваний, частота/стоимость поллинга и дедуп при наплыве.
Связано: [architecture.md](specs/architecture.md) → «Отслеживание
загрузки»/«Хранилище», пакеты `worker`, `store`, `qbt`, `llm`.
## Средний
### Словарь единого языка (ubiquitous language)
Свести термины домена в один глоссарий, чтобы пользователь, документация,
код и агент говорили на одном языке: загрузка, раздача, распознавание, матч,
кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и
т.д. — русский термин, английский идентификатор в коде, краткое определение.
Сейчас наименования расходятся между спеками, UI и кодом, и в диалоге с
агентом приходится каждый раз сверять понятия. Глоссарий — источник истины
по именам; на нём же строится агент-ревьювер наименований (см.
[«Агенты-ревьюверы качества»](#агенты-ревьюверы-качества-наименования-архитектура-конвенции-стиль)).
Связано: [docs/conventions](conventions/README.md) (кросс-каттинг),
[architecture.md](specs/architecture.md) (домен), новый файл-глоссарий.
### Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль)
Набор узких сабагентов-ревьюверов поверх ревью-процесса из `CLAUDE.md`,
каждый со своей оптикой: соответствие наименований словарю единого языка,
соблюдение архитектурных границ (единое ядро/тонкие транспорты, инварианты
безопасности данных), конвенций (ошибки, логирование, конфиг, TZ), стиля
кода на высоком уровне и поиск дублирования. Запускаются как чекпоинт перед
`archive`/коммитом. Развивает ревью-процесс OpenSpec в сторону
воспроизводимых автоматических проверок, не заменяя человеческое ревью.
Связано: `CLAUDE.md` (ревью-процесс, конвенции),
[docs/conventions](conventions/README.md),
[«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language).
### Пересмотр набора capabilities и рефакторинг спек
Деление capabilities в OpenSpec сложилось по ходу миграции и смешивает
разные действия в одной спеке. Пример: `recognition` держит и разбор через
LLM, и **сверку с внешними базами** — а «поиск в базе» и «подтверждение
матча официальным id» суть разные действия, значит и разные capability.
Нужно пересмотреть набор и границы, чтобы имя capability отвечало одному
поведению:
- `recognition` — только разбор сигналов через LLM (план, тип, название,
файлы → серии);
- отдельные capability под работу с метабазами: поиск записей во внешних
базах и сверку/подтверждение матча (разнести пока смешанное в
`recognition`);
- `web-ui` — только общее оформление, дизайн-система и общие компоненты
страниц;
- `review` — весь процесс ревью после распознавания и матча (мигрировать из
[review-ux.md](specs/review-ux.md); сейчас поведение ревью не в OpenSpec).
Работа чисто по спекам (границы, RENAMED/MOVED requirements), код не
трогаем. Ценно тем, что снимает путаницу «какой capability трогать» на
каждой задаче.
Связано: `CLAUDE.md` (SDD, миграция capabilities), openspec/specs
(`recognition`, `web-ui`), [review-ux.md](specs/review-ux.md),
[«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language).
### Сила совпадения кандидата и пересмотр распознавания/матчинга _(идея)_
Сейчас у кандидата метабазы нет метрики силы совпадения (`metadata_candidate`
хранит provider/id/title/year/url), а решение «авто vs review» — по правилу
«единственный сильный матч + валидация», не по числовой уверенности. Для
ревью это значит: список кандидатов нечем отсортировать/подсветить по
уверенности — берём порядок сбора. Идея — ввести на этапе матча **силу
совпадения кандидата** (точное совпадение названия+года vs частичное) для
сортировки и подсказки в UI. Шире — отдельно продумать **сам процесс
распознавания и матчинга**: границы «разбор LLM / поиск в базе / сверка»,
что храним у кандидата, как считаем и показываем уверенность. Требует
проработки перед реализацией.
Связано: [recognition.md](specs/recognition.md) (модель уверенности),
[ADR-2026-06-13-auto-link-requires-db-match](adr/ADR-2026-06-13-auto-link-requires-db-match.md),
[«Ревью: выбор источника совпадения»](#ревью-выбор-источника-совпадения-и-предпросмотр),
[«Пересмотр набора capabilities»](#пересмотр-набора-capabilities-и-рефакторинг-спек).
### История переходов загрузки
Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто
инициировал — воркер, человек, сверка), а не только текущее состояние.
Сейчас по задаче виден лишь актуальный статус, а разбор «как мы сюда попали»
идёт по логам сервера. Отдельная таблица истории даёт лог переходов в
карточке/расширенной информации и фундамент для метрик длительности стадий.
Естественно ложится на собственный идентификатор загрузки.
Связано: детальный экран загрузки (`/download/{id}`) уже реализован — лог
переходов ложится в него;
[drafts/logical-title-model.md](drafts/logical-title-model.md) §5.4 (схема
`state_transition`, actor `worker|human|reconcile`),
[workflow.md](specs/workflow.md) (граф состояний),
[database.md](specs/database.md), пакеты `worker`, `store`.
### Машина состояний на go-библиотеке
Сейчас FSM реализована вручную в `worker`. Выбрать подходящую go-библиотеку
для описания воркфлоу/машины состояний и перевести переходы на неё — ради
декларативности, проверяемости переходов и единого места правды. Кандидаты
для оценки: `looplab/fsm`, `qmuntal/stateless` (и аналоги). Граф и переходы
уже формализованы — переносим один в один.
Связано: [workflow.md](specs/workflow.md) (текущий граф состояний).
### Привязка уведомлений к источнику в ботах (мульти-бот)
Уведомления и запросы подтверждения должен получать тот, кто прислал
загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней.
Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и
др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся
**единым для всех** и точкой правды по функциональности (боты — тонкие
адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и
идентификатор отправителя, маршрутизировать пинги по нему.
Связано: [review-ux.md](specs/review-ux.md) (разделение труда транспортов,
веб = точные правки), [architecture.md](specs/architecture.md) →
«Транспорты».
### Улучшения UI: показывать матч с записью метабазы
Web-сторона реализована: страница загрузки `/download/{id}` и экран ревью
показывают, **с какой именно записью** метабазы (TMDB/TVDB/IMDb) сматчилась
загрузка — провайдер, id и ссылку на запись. Осталось довести то же в
**Telegram**: в уведомлениях/подтверждениях показывать запись матча (название,
год, провайдер-id, ссылку), чтобы ошибочную привязку было видно и из бота.
Полный выбор источника в вебе — см. [«Ревью: выбор источника
совпадения»](#ревью-выбор-источника-совпадения-и-предпросмотр).
Связано: [review-ux.md](specs/review-ux.md), [recognition.md](specs/recognition.md)
(матч в базе), [architecture.md](specs/architecture.md) → «Транспорты».
### Аниме с абсолютной нумерацией
Релизы аниме часто нумеруют серии сквозным числом (`#137`) без сезонов, а
Jellyfin ждёт `SxxEyy`. Нужен пересчёт абсолютной нумерации в сезон/серию —
надёжнее всего через TVDB (там есть absolute order). Отдельный крайний
случай распознавания; на стороне ревью — веб-хелпер «absolute → S·E».
Связано: [recognition.md](specs/recognition.md) (конвейер, сезон-паки),
[jellyfin-layout.md](specs/jellyfin-layout.md) (нумерация серий),
[review-ux.md](specs/review-ux.md) (крайние сценарии).
### Добавление торрентов файлом/ссылкой — «единое окно»
Поддержать источники помимо magnet: `.torrent`-файл и URL (отдаём их в
qBittorrent, без исходящих запросов на пользовательский URL — SSRF
исключён). Идеал — одно поле «единого окна»: кидаем туда текст или файл, а
сервис сам разбирает, что это (magnet / ссылка / .torrent / сообщение
бота), и заводит загрузку.
Связано: [architecture.md](specs/architecture.md) → «Транспорты»
(`source_type = magnet|torrent|url` уже в схеме), пакет `ingest` (сейчас
поддержан только magnet).
### Бэкап SQLite
`architecture.md` требует «бекапить data-том», но *как* — не описано. Без
понятной стратегии сбой или редеплой стирают всё in-flight состояние.
Зафиксировать решение и реализовать: периодический `VACUUM INTO` в
`/data/backups` по расписанию (с ротацией) либо потоковая репликация
(litestream). Лучше сделать, пока БД маленькая.
Связано: [architecture.md](specs/architecture.md) → «Деплой» (data-том,
«бекапить-и-не-терять»), пакет `store`.
### Глубокий healthcheck и статус зависимостей
`/healthz` проверяет только сам сервис. Если qBittorrent, LLM или метабаза
недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка
ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent
недоступен»), чтобы причина простоя была видна сразу.
Связано: [architecture.md](specs/architecture.md) → «Деплой» (healthcheck),
пакеты `qbt`, `llm`, `metadata`, `httpapi`.
### Обучение на правках человека (few-shot из прошлых ревью)
Когда человек поправил матч, тип или нумерацию — сохранять это как пример и
подмешивать похожие в будущие промпты. Системно повышает точность на «твоих»
трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой
верификации, но дешевле: учимся на уже собранных `hint`/`override`.
Связано: [recognition.md](specs/recognition.md) (конвейер, промпт),
[«Многоступенчатая верификация»](#многоступенчатая-верификация-привязки-идея),
[architecture.md](specs/architecture.md) → «Хранилище» (`hint`, `override`).
## Низкий
### Мгновенные обновления через SSE
Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто
и работает, но с задержкой в интервал опроса и холостыми запросами.
Перевести динамический контент (прогресс загрузки, смена статуса, раздача)
на Server-Sent Events, чтобы обновления приходили почти мгновенно и без
лишнего поллинга. Поллинг работает, поэтому это улучшение, а не блокер; SSE —
один долгоживущий ответ на соединение, ложится на server-rendered UI без
тяжёлого фронтенда.
Связано: [architecture.md](specs/architecture.md) → «Транспорты»,
[review-ux.md](specs/review-ux.md), пакет `httpapi`.
### Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)
По калибровке болей (2026-07-02,
[drafts/logical-title-model.md](drafts/logical-title-model.md) §6.3) —
**не боль**, из приоритета выпало. Сосуществование версий доступно уже
сейчас (Jellyfin multi-version, другой целевой путь), коллизия на тот же
путь штатно уходит в review. Явный replace (undo старого хардлинка → lay
нового → супересид владения путём) — отдельный change, если/когда станет
болью.
Связано: [«Раздачи с докачиванием»](#раздачи-с-докачиванием-слияние-при-повторном-добавлении),
[jellyfin-layout.md](specs/jellyfin-layout.md) (never-overwrite, коллизия).
### Многоступенчатая верификация привязки _(идея)_
Несколько раз извлекать данные из раздачи и контекста разными промптами,
искать в метабазах, затем сводить результаты в общий вердикт
(голосование/консенсус) — выше точность ценой нескольких вызовов LLM и
запросов к базам. Требует проработки: когда включать, как мерджить
расхождения, стоимость/латентность.
Связано: [recognition.md](specs/recognition.md) (конвейер и модель
уверенности).
### Выбор из нескольких находок метабазы в Telegram
Когда распознавание даёт несколько подходящих кандидатов в метабазе,
предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча
брать первый/лучший. Веб остаётся точкой точных правок (полный выбор
источника — см. [«Ревью: выбор источника
совпадения»](#ревью-выбор-источника-совпадения-и-предпросмотр)), бот —
быстрый выбор из готового короткого списка.
Связано: [review-ux.md](specs/review-ux.md) (боты — быстрые действия, веб —
точные правки), [recognition.md](specs/recognition.md) (кандидаты матча).
### Проверка свободного места перед copy-fallback
Когда хардлинк невозможен (`EXDEV`/`ENOTSUP`/…), `layout` копирует файл,
дублируя место на диске. На забитом диске это упрётся в полку посреди
раскладки. Перед копированием проверять доступное место и при нехватке
внятно уходить в `failed` с понятной причиной, а не падать на полпути.
Связано: [architecture.md](specs/architecture.md) → «Раскладка файлов»
(фолбэк-копирование), пакет `layout`.
### Кэш метабаз (и опционально LLM)
Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и
тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать
заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее
полезен, т.к. вход меняется подсказками).
Связано: [recognition.md](specs/recognition.md) (сверка с базой), пакеты
`metadata`, `llm`.
### guessit как сервис-спутник _(идея)_
`go-ptn` слабее питоновского `guessit`. Если точности пред-парса не
хватит — завернуть `guessit` в крошечный HTTP-сервис (один файл,
поставляется рядом с бинарём jellybit) и спрашивать его на шаге
пред-парса. Сохраняет «доставку копированием»: два файла вместо одного.
Связано: [recognition.md](specs/recognition.md) → «На будущее» (пред-парс).
### Завершение загрузки через webhook _(идея)_
Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд.
Альтернатива: «Run external program on torrent completion» в qBittorrent
дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом
qBittorrent. Решим по опыту эксплуатации.
Связано: [architecture.md](specs/architecture.md) → «Отслеживание загрузки»,
пакет `worker`.
### Авторизация веб-UI (на будущее)
Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей
(`http.trusted_subnets`) — как умеет qBittorrent. Если понадобится защита:
токен/Basic в самом приложении или вынос за reverse-proxy с
аутентификацией.
Связано: [architecture.md](specs/architecture.md) → «Транспорты» (доступ к
веб-UI), пакет `httpapi`.
### Современный Web-UI как PWA
Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое,
отзывчивое, удобное с телефона). Текущий server-rendered UI функционален,
поэтому это улучшение, а не блокер; большой объём работы.
Связано: [review-ux.md](specs/review-ux.md) (веб = точные правки),
пакет `httpapi`.