diff --git a/CLAUDE.md b/CLAUDE.md index 03e8a1c..70687cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,17 +93,19 @@ OpenSpec (пилот — `ingest`). До переноса источник ис ## Задачи и беклог -- **Единственный источник беклога — Tududi**, проект `jellybit` (MCP-сервер - `tududi`, project_id 14). Там задачи с приоритетами (высокий/средний/ - низкий) и описанием (контекст, принятые решения, ссылки на спеки/ADR/ - черновики в теле задачи). Ищи, заводи и закрывай задачи через - MCP-инструменты `tududi` (`list_tasks`, `create_task`, `update_task`, - `complete_task`, `search`). +- **Единственный источник беклога — каталог [docs/backlog/](docs/backlog/README.md)**: + одна задача = один markdown-файл (`docs/backlog/.md`), плюс + индекс [README.md](docs/backlog/README.md) со списком по приоритетам + (высокий/средний/низкий) и хуками. В теле файла — контекст, принятые + решения, шаги и ссылки на спеки/ADR/черновики. Заводи задачу новым файлом + и строкой в индексе; закрытую (реализованную) — удаляй, суть переезжает в + `docs/specs`/`docs/adr`. - Спекулятивные задачи (ещё без решения «делаем») помечены префиксом `[идея]` в названии — их сперва прорабатываем. -- Отдельного файла-беклога в репозитории больше нет: `docs/backlog.md` - перенесён в Tududi. Старая версия при необходимости доступна в истории - git. +- **Tududi — только инбокс сырых идей** (проект `jellybit`, project_id 14). + Беклог там больше не ведём; идея из Tududi становится задачей, когда её + оформляют файлом в `docs/backlog/`. Прежняя единая `docs/backlog.md` + доступна в истории git. ## Язык diff --git a/docs/backlog/README.md b/docs/backlog/README.md new file mode 100644 index 0000000..bc842a5 --- /dev/null +++ b/docs/backlog/README.md @@ -0,0 +1,72 @@ +# Беклог + +Единый список будущих задач по проекту: то, что уже решили сделать, и идеи, +которые ещё надо обдумать. Это **источник истины по беклогу** — одна задача = один +файл в этом каталоге. Не план реализации и не спецификация: принятое и +реализованное переезжает в [`docs/specs`](../specs)/[`docs/adr`](../adr), а сам +пункт беклога удаляется. + +Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку. +Спекулятивные пункты (ещё без решения «делаем») помечены префиксом `[идея]` в +названии — их сперва надо проработать. Пункты, помеченные _(ревью 2026-07-08)_, +пришли из тщательного ревью ingest/worker/жизненного цикла (см. общий тег в теле). + +Tududi (проект `jellybit`) больше **не** держит беклог — он служит только +инбоксом сырых идей. Прежде чем идея станет задачей, её оформляют файлом здесь. + +## Высокий + +- [Проблема второго сезона (сходимость папки сериала)](vtoroy-sezon-shodimost-papki.md) — Второй/третий сезон должен ложиться в ТУ ЖЕ папку сериала, а не заводить рядом почти… +- [Раздачи с докачиванием (merge при повторном добавлении)](merge-dokachivanie.md) — Свежий сериал раздают по мере выхода: торрент с 5 из 10 эпизодов позже перезаливают… +- [Удаление средствами jellybit («единое окно», path 2)](udalenie-edinoe-okno.md) — Распознавание ручного удаления и пометка рассинхрона уже сделаны (state-reconciliation… +- [Ретеншн и очистка БД](retention-ochistka-bd.md) — Терминальные задачи (done/cancelled/failed/reverted), их попытки recognition с сырыми… +- [Eval-харнес распознавания (корпус кейсов + метрика точности)](eval-harness-raspoznavaniya.md) — Распознавание — ядро продукта, но смена модели или правка промпта сейчас вслепую… +- [НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)](masshtab-100-zagruzok.md) — Потолок по нагрузке нигде не зафиксирован: воркер, поллинг qBittorrent, пул LLM-вызовов и… +- [Гейт дозаписи хешей в dedup-ветке CreateDownloadIfNoActive (F1)](review-f1-gate-dozapisi-heshey.md) — dedup-ветка дописывает все хеши в найденную задачу без гарда — риск инварианта ≤1 активной _(ревью 2026-07-08)_ +- [Readiness-preflight: запрет авто-раскладки недокачанных файлов (MAJOR-5)](review-major5-readiness-preflight.md) — САМОЕ ОПАСНОЕ: авто-раскладка недокачанных файлов рвёт целостность библиотеки _(ревью 2026-07-08)_ +- [Retry/stall семантика: сброс базиса таймаута + простой от начала, а не от возраста торрента (MAJOR-1, MAJOR-2)](review-major1-2-retry-stall.md) — таймаут и простой отсчитываются от возраста торрента, а не от начала загрузки _(ревью 2026-07-08)_ +- [Восстановление zombie downloading при пропаже источника из qBittorrent (MAJOR-3)](review-major3-zombie-downloading.md) — торрент пропал из qBittorrent в downloading → задача вечный зомби, никто не двигает _(ревью 2026-07-08)_ + +## Средний + +- [Словарь единого языка (ubiquitous language)](ubiquitous-language-slovar.md) — Свести термины домена в один глоссарий, чтобы пользователь, документация, код и агент… +- [Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль)](agenty-revyuvery-kachestva.md) — Набор узких сабагентов-ревьюверов поверх ревью-процесса из CLAUDE +- [[идея] Сила совпадения кандидата и пересмотр распознавания/матчинга](sila-sovpadeniya-kandidata.md) — ИДЕЯ (сперва проработать) +- [История переходов загрузки](istoriya-perehodov-zagruzki.md) — Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто инициировал… +- [Машина состояний на go-библиотеке](mashina-sostoyaniy-biblioteka.md) — Сейчас FSM реализована вручную в worker +- [Привязка уведомлений к источнику в ботах (мульти-бот)](uvedomleniya-multi-bot.md) — Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор… +- [Улучшения UI: показывать матч с записью метабазы в Telegram](telegram-match-metabazy.md) — Web-сторона реализована: страница загрузки /download/{id} и экран ревью показывают, с… +- [[идея] Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки](slozhnye-serialnye-razdachi.md) — ИДЕЯ (проработать крайние случаи) +- [Аниме с абсолютной нумерацией](anime-absolyutnaya-numeraciya.md) — Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а Jellyfin ждёт… +- [Добавление торрентов файлом/ссылкой — «единое окно»](dobavlenie-edinoe-okno.md) — Поддержать источники помимо magnet: +- [Бэкап SQLite](backup-sqlite.md) — architecture +- [Глубокий healthcheck и статус зависимостей](healthcheck-zavisimosti.md) — /healthz проверяет только сам сервис +- [Обучение на правках человека (few-shot из прошлых ревью)](obuchenie-na-pravkah.md) — Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать… +- [Гейт авто-раскладки по confidence: спека vs код](gate-confidence-spec-vs-code.md) — Аудит спек↔код (2026-07-03) нашёл расхождение в модели уверенности +- [Привязка внешних субтитров к серии (сериалы)](vneshnie-subtitry.md) — Аудит спек↔код (2026-07-03): спека recognition требует «внешние субтитры SHALL… +- [Раздачи-копии диска (DVD/BluRay: VIDEO_TS/BDMV)](disk-kopii-video-ts-bdmv.md) — Иногда для очень редких фильмов скачивается не один видеофайл, а полная копия диска… +- [Сделать фавиконку для jellybit](favicon.md) — мелкая косметика: иконка вкладки/PWA для веб-UI +- [Sweep застрявших linking при рестарте + фикс persist-failure (MAJOR-4)](review-major4-sweep-linking.md) — задачи застревают в linking при рестарте; заодно фикс persist-failure _(ревью 2026-07-08)_ +- [Defer из catched → лимбо → необратимый deleted (MAJOR-6)](review-major6-defer-catched.md) — Defer из ещё-не-добавленного catched уводит задачу в необратимый deleted _(ревью 2026-07-08)_ +- [processCatched: promote-without-add если торрент уже в qBittorrent (F2)](review-f2-promote-without-add.md) — торрент уже в qBittorrent → processCatched зациклен на Add вместо promote _(ревью 2026-07-08)_ +- [Cancel во время add оставляет неуправляемый торрент в qBittorrent (F3/NIT-13)](review-f3-cancel-during-add.md) — Cancel во время add оставляет неуправляемый торрент в qBittorrent _(ревью 2026-07-08)_ +- [transition() глотает ошибки перед созданием хардлинков (MINOR-7)](review-minor7-transition-errors.md) — transition() глотает ошибку перед хардлинками — задача застревает в review _(ревью 2026-07-08)_ +- [.torrent поверх magnet при дедупе теряет байты — потерян upgrade-путь (F6)](review-f6-torrent-over-magnet.md) — .torrent поверх magnet при дедупе теряет байты — потерян upgrade-путь _(ревью 2026-07-08)_ + +## Низкий + +- [Панель действий ревью вне htmx-свопа блока источника](panel-review-vne-swap.md) — При выборе источника одним кликом обновляется только блок источника (#source-block)… +- [Мгновенные обновления через SSE](sse-obnovleniya.md) — Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает… +- [Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)](versii-kachestvo-repaki.md) — По калибровке болей (2026-07-02) — не боль, из приоритета выпало +- [[идея] Многоступенчатая верификация привязки](mnogostupenchataya-verifikaciya.md) — ИДЕЯ (требует проработки) +- [Выбор из нескольких находок метабазы в Telegram](telegram-vybor-nahodok.md) — Когда распознавание даёт несколько подходящих кандидатов в метабазе, предлагать их в… +- [Проверка свободного места перед copy-fallback](svobodnoe-mesto-copy-fallback.md) — Когда хардлинк невозможен (EXDEV/ENOTSUP/…), layout копирует файл, дублируя место на диске +- [Кэш метабаз (и опционально LLM)](kesh-metabaz.md) — Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же… +- [[идея] guessit как сервис-спутник](guessit-sputnik.md) — ИДЕЯ +- [[идея] Завершение загрузки через webhook](webhook-zavershenie-zagruzki.md) — ИДЕЯ (решим по опыту эксплуатации) +- [Авторизация веб-UI (на будущее)](avtorizaciya-web-ui.md) — Решено для v1: без авторизации в доверенной LAN, опц +- [Современный Web-UI как PWA](web-ui-pwa.md) — Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое… +- [Идентичность инфохэшей: split v1/v2 одного торрента + крафт-магнет отравляет владение (F4, F5)](review-f4-f5-infohash-identity.md) — split v1/v2 идентичность и крафт-магнет отравляют владение инфохэшами _(ревью 2026-07-08)_ +- [Приём/UI: мелкие фиксы границ и парсинга (F7, F8, F9, F10, N2)](review-f7-f10-ingest-ui-fixes.md) — мелочи приёма/UI: oversized→500, гонки дедупа, парсинг magnet, cap контекста, руны _(ревью 2026-07-08)_ +- [Жизненный цикл: мелкие находки (MINOR-8, MINOR-9, NIT-11, NIT-12)](review-lifecycle-minor.md) — claim-token распознавания, I/O под глобальным mutex, мелкие lookup/retry _(ревью 2026-07-08)_ +- [Нити приёма: NoName в контексте, устаревшие комментарии, лог без причины, bencode-аллокации (N1, N3, N4, N5)](review-ingest-nits.md) — косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_ diff --git a/docs/backlog/agenty-revyuvery-kachestva.md b/docs/backlog/agenty-revyuvery-kachestva.md new file mode 100644 index 0000000..74d6a40 --- /dev/null +++ b/docs/backlog/agenty-revyuvery-kachestva.md @@ -0,0 +1,7 @@ +# Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль) + +**Приоритет:** средний + +Набор узких сабагентов-ревьюверов поверх ревью-процесса из CLAUDE.md, каждый со своей оптикой: соответствие наименований словарю единого языка, соблюдение архитектурных границ (единое ядро/тонкие транспорты, инварианты безопасности данных), конвенций (ошибки, логирование, конфиг, TZ), стиля кода и поиск дублирования. Запускаются как чекпоинт перед archive/коммитом. Развивает ревью-процесс OpenSpec в сторону воспроизводимых автопроверок, не заменяя человеческое ревью. + +Связано: CLAUDE.md (ревью-процесс, конвенции), docs/conventions, «Словарь единого языка». diff --git a/docs/backlog/anime-absolyutnaya-numeraciya.md b/docs/backlog/anime-absolyutnaya-numeraciya.md new file mode 100644 index 0000000..9e9cce2 --- /dev/null +++ b/docs/backlog/anime-absolyutnaya-numeraciya.md @@ -0,0 +1,7 @@ +# Аниме с абсолютной нумерацией + +**Приоритет:** средний + +Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а Jellyfin ждёт SxxEyy. Нужен пересчёт абсолютной нумерации в сезон/серию — надёжнее всего через TVDB (там есть absolute order). Отдельный крайний случай распознавания; на стороне ревью — веб-хелпер «absolute → S·E». + +Связано: specs/recognition.md (конвейер, сезон-паки), specs/jellyfin-layout.md (нумерация серий), specs/review-ux.md. diff --git a/docs/backlog/avtorizaciya-web-ui.md b/docs/backlog/avtorizaciya-web-ui.md new file mode 100644 index 0000000..b7b2b38 --- /dev/null +++ b/docs/backlog/avtorizaciya-web-ui.md @@ -0,0 +1,7 @@ +# Авторизация веб-UI (на будущее) + +**Приоритет:** низкий + +Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей (http.trusted_subnets) — как умеет qBittorrent. Если понадобится защита: токен/Basic в самом приложении или вынос за reverse-proxy с аутентификацией. + +Связано: specs/architecture.md → «Транспорты» (доступ к веб-UI), пакет httpapi. diff --git a/docs/backlog/backup-sqlite.md b/docs/backlog/backup-sqlite.md new file mode 100644 index 0000000..b25e8a3 --- /dev/null +++ b/docs/backlog/backup-sqlite.md @@ -0,0 +1,7 @@ +# Бэкап SQLite + +**Приоритет:** средний + +architecture.md требует «бекапить data-том», но как — не описано. Без понятной стратегии сбой или редеплой стирают всё in-flight состояние. Зафиксировать решение и реализовать: периодический VACUUM INTO в /data/backups по расписанию (с ротацией) либо потоковая репликация (litestream). Лучше сделать, пока БД маленькая. + +Связано: specs/architecture.md → «Деплой» (data-том), пакет store. diff --git a/docs/backlog/disk-kopii-video-ts-bdmv.md b/docs/backlog/disk-kopii-video-ts-bdmv.md new file mode 100644 index 0000000..9b1c94f --- /dev/null +++ b/docs/backlog/disk-kopii-video-ts-bdmv.md @@ -0,0 +1,7 @@ +# Раздачи-копии диска (DVD/BluRay: VIDEO_TS/BDMV) + +**Приоритет:** средний + +Иногда для очень редких фильмов скачивается не один видеофайл, а полная копия диска — структура VIDEO_TS/ (DVD) или BDMV/ (BluRay). Сейчас распознавание и раскладка заточены под пофайловый разбор, а тут «фильм» — это каталог целиком. Jellyfin такие раскладки поддерживает (папка фильма с вложенным VIDEO_TS/BDMV). Нужно: распознать, что раздача — образ диска (по наличию VIDEO_TS/BDMV), не разбирать её по отдельным VOB/m2ts как серии, разложить весь каталог хардлинками в папку фильма (Название (Год)/VIDEO_TS/…). Крайний, но реальный случай; частота низкая. + +Связано: specs/recognition.md (роли файлов), specs/jellyfin-layout.md (раскладка фильма), пакеты recognize, layout. diff --git a/docs/backlog/dobavlenie-edinoe-okno.md b/docs/backlog/dobavlenie-edinoe-okno.md new file mode 100644 index 0000000..4c22b44 --- /dev/null +++ b/docs/backlog/dobavlenie-edinoe-okno.md @@ -0,0 +1,7 @@ +# Добавление торрентов файлом/ссылкой — «единое окно» + +**Приоритет:** средний + +Поддержать источники помимо magnet: .torrent-файл и URL (отдаём их в qBittorrent, без исходящих запросов на пользовательский URL — SSRF исключён). Идеал — одно поле «единого окна»: кидаем туда текст или файл, а сервис сам разбирает, что это (magnet / ссылка / .torrent / сообщение бота), и заводит загрузку. + +Связано: specs/architecture.md → «Транспорты» (source_type = magnet|torrent|url уже в схеме), пакет ingest (сейчас поддержан только magnet). diff --git a/docs/backlog/eval-harness-raspoznavaniya.md b/docs/backlog/eval-harness-raspoznavaniya.md new file mode 100644 index 0000000..506ae04 --- /dev/null +++ b/docs/backlog/eval-harness-raspoznavaniya.md @@ -0,0 +1,7 @@ +# Eval-харнес распознавания (корпус кейсов + метрика точности) + +**Приоритет:** высокий + +Распознавание — ядро продукта, но смена модели или правка промпта сейчас вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по нему с метрикой точности (тип/название/год/нумерация). Тогда можно сравнивать LLM-провайдеры и версии промпта по числам. Прогон — отдельной командой (jellybit eval или тестом), на фикстурах, без реального qBittorrent. + +Связано: specs/recognition.md (конвейер, модель уверенности), пакет recognize. diff --git a/docs/backlog/favicon.md b/docs/backlog/favicon.md new file mode 100644 index 0000000..526e17b --- /dev/null +++ b/docs/backlog/favicon.md @@ -0,0 +1,5 @@ +# Сделать фавиконку для jellybit + +**Приоритет:** средний + +_Описание не заполнено._ diff --git a/docs/backlog/gate-confidence-spec-vs-code.md b/docs/backlog/gate-confidence-spec-vs-code.md new file mode 100644 index 0000000..b11b112 --- /dev/null +++ b/docs/backlog/gate-confidence-spec-vs-code.md @@ -0,0 +1,7 @@ +# Гейт авто-раскладки по confidence: спека vs код + +**Приоритет:** средний + +Аудит спек↔код (2026-07-03) нашёл расхождение в модели уверенности. Спека recognition утверждает, что самооценка LLM confidence — вспомогательный сигнал, НЕ единственный гейт: при подтверждённом матче + чистой валидации + согласованности сигналов авто-раскладка допускается. Код же (internal/recognize/validate.go, confidence < AutoThreshold, дефолт 0.85) делает confidence жёстким блокирующим условием: план с матчем и чистой валидацией, но confidence 0.5 уйдёт в review вопреки спеке. Определиться: признать порог AutoThreshold в спеке как легитимный гейт (скорее так — код его осознанно ввёл конфигом) либо ослабить код. Заодно AutoThreshold как конфигурируемый гейт спекой не описан. + +Связано: openspec/specs/recognition, ADR-2026-06-13-auto-link-requires-db-match, пакет recognize. diff --git a/docs/backlog/guessit-sputnik.md b/docs/backlog/guessit-sputnik.md new file mode 100644 index 0000000..1ae7020 --- /dev/null +++ b/docs/backlog/guessit-sputnik.md @@ -0,0 +1,7 @@ +# [идея] guessit как сервис-спутник + +**Приоритет:** низкий + +ИДЕЯ. go-ptn слабее питоновского guessit. Если точности пред-парса не хватит — завернуть guessit в крошечный HTTP-сервис (один файл, поставляется рядом с бинарём jellybit) и спрашивать его на шаге пред-парса. Сохраняет «доставку копированием»: два файла вместо одного. + +Связано: specs/recognition.md → «На будущее» (пред-парс). diff --git a/docs/backlog/healthcheck-zavisimosti.md b/docs/backlog/healthcheck-zavisimosti.md new file mode 100644 index 0000000..32c74e0 --- /dev/null +++ b/docs/backlog/healthcheck-zavisimosti.md @@ -0,0 +1,7 @@ +# Глубокий healthcheck и статус зависимостей + +**Приоритет:** средний + +/healthz проверяет только сам сервис. Если qBittorrent, LLM или метабаза недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent недоступен»), чтобы причина простоя была видна сразу. + +Связано: specs/architecture.md → «Деплой» (healthcheck), пакеты qbt, llm, metadata, httpapi. diff --git a/docs/backlog/istoriya-perehodov-zagruzki.md b/docs/backlog/istoriya-perehodov-zagruzki.md new file mode 100644 index 0000000..ece1973 --- /dev/null +++ b/docs/backlog/istoriya-perehodov-zagruzki.md @@ -0,0 +1,7 @@ +# История переходов загрузки + +**Приоритет:** средний + +Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто инициировал — воркер, человек, сверка), а не только текущее состояние. Сейчас по задаче виден лишь актуальный статус, разбор «как мы сюда попали» идёт по логам сервера. Отдельная таблица истории даёт лог переходов в карточке/расширенной информации и фундамент для метрик длительности стадий. Естественно ложится на собственный идентификатор загрузки и уже реализованный экран /download/{id}. + +Связано: drafts/logical-title-model.md §5.4 (state_transition, actor worker|human|reconcile), specs/workflow.md, specs/database.md, пакеты worker, store. diff --git a/docs/backlog/kesh-metabaz.md b/docs/backlog/kesh-metabaz.md new file mode 100644 index 0000000..d91be05 --- /dev/null +++ b/docs/backlog/kesh-metabaz.md @@ -0,0 +1,7 @@ +# Кэш метабаз (и опционально LLM) + +**Приоритет:** низкий + +Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее полезен, т.к. вход меняется подсказками). + +Связано: specs/recognition.md (сверка с базой), пакеты metadata, llm. diff --git a/docs/backlog/mashina-sostoyaniy-biblioteka.md b/docs/backlog/mashina-sostoyaniy-biblioteka.md new file mode 100644 index 0000000..e039e2b --- /dev/null +++ b/docs/backlog/mashina-sostoyaniy-biblioteka.md @@ -0,0 +1,7 @@ +# Машина состояний на go-библиотеке + +**Приоритет:** средний + +Сейчас FSM реализована вручную в worker. Выбрать подходящую go-библиотеку для описания воркфлоу/машины состояний и перевести переходы на неё — ради декларативности, проверяемости переходов и единого места правды. Кандидаты для оценки: looplab/fsm, qmuntal/stateless (и аналоги). Граф и переходы уже формализованы — переносим один в один. + +Связано: specs/workflow.md (текущий граф состояний). diff --git a/docs/backlog/masshtab-100-zagruzok.md b/docs/backlog/masshtab-100-zagruzok.md new file mode 100644 index 0000000..7d51b63 --- /dev/null +++ b/docs/backlog/masshtab-100-zagruzok.md @@ -0,0 +1,7 @@ +# НФТ: масштаб до 100 одновременных загрузок (потолок — 1000) + +**Приоритет:** высокий + +Потолок по нагрузке нигде не зафиксирован: воркер, поллинг qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз». Записать в НФТ целевой ориентир — архитектура держит до 100 одновременных загрузок в работе (приём → распознавание → раскладка), план-максимум — 1000. Сама запись требования дешева и высокоценна: задаёт рамку для решений ниже. Отдельно (дороже) — аудит узких мест: одиночное соединение SQLite и сериализация записи, конкурентность воркера и лимит параллельных распознаваний, частота/стоимость поллинга и дедуп при наплыве. + +Связано: specs/architecture.md → «Отслеживание загрузки»/«Хранилище», пакеты worker, store, qbt, llm. diff --git a/docs/backlog/merge-dokachivanie.md b/docs/backlog/merge-dokachivanie.md new file mode 100644 index 0000000..8c48357 --- /dev/null +++ b/docs/backlog/merge-dokachivanie.md @@ -0,0 +1,14 @@ +# Раздачи с докачиванием (merge при повторном добавлении) + +**Приоритет:** высокий + +Свежий сериал раздают по мере выхода: торрент с 5 из 10 эпизодов позже перезаливают целиком, пользователь добавляет раздачу повторно. Новая загрузка приходит в ту же папку за счёт правила сходимости, а раскладка становится merge — доложить только недостающее. Существующие пути не трогаем (never-overwrite, владение у старой загрузки), новые кладём (владеет новая). Split-ownership сезона принят как норма per-path модели; обе раздачи сидируют независимо. + +Шаги: +- в плане раскладки отличать «путь занят живой ссылкой того же матча» (→ пропустить) от настоящей коллизии (→ review) +- merge-раскладка: существующее пропустить, недостающее доложить +- показать итог в карточке: сколько доложено, сколько уже было +- решить «слияние загрузок» при перезаливе (одна строка download + новый infohash vs новая загрузка) — открытый вопрос черновика §10 + +Зависит от правила сходимости («Проблема второго сезона»), выигрывает от ULID-идентичности. +Связано: drafts/logical-title-model.md §6.2, specs/jellyfin-layout.md, specs/workflow.md diff --git a/docs/backlog/mnogostupenchataya-verifikaciya.md b/docs/backlog/mnogostupenchataya-verifikaciya.md new file mode 100644 index 0000000..e4d5895 --- /dev/null +++ b/docs/backlog/mnogostupenchataya-verifikaciya.md @@ -0,0 +1,7 @@ +# [идея] Многоступенчатая верификация привязки + +**Приоритет:** низкий + +ИДЕЯ (требует проработки). Несколько раз извлекать данные из раздачи и контекста разными промптами, искать в метабазах, затем сводить результаты в общий вердикт (голосование/консенсус) — выше точность ценой нескольких вызовов LLM и запросов к базам. Проработать: когда включать, как мерджить расхождения, стоимость/латентность. + +Связано: specs/recognition.md (конвейер и модель уверенности). diff --git a/docs/backlog/obuchenie-na-pravkah.md b/docs/backlog/obuchenie-na-pravkah.md new file mode 100644 index 0000000..37325f1 --- /dev/null +++ b/docs/backlog/obuchenie-na-pravkah.md @@ -0,0 +1,7 @@ +# Обучение на правках человека (few-shot из прошлых ревью) + +**Приоритет:** средний + +Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать похожие в будущие промпты. Системно повышает точность на «твоих» трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой верификации, но дешевле: учимся на уже собранных hint/override. + +Связано: specs/recognition.md (конвейер, промпт), «Многоступенчатая верификация», specs/architecture.md → «Хранилище» (hint, override). diff --git a/docs/backlog/panel-review-vne-swap.md b/docs/backlog/panel-review-vne-swap.md new file mode 100644 index 0000000..01a62ea --- /dev/null +++ b/docs/backlog/panel-review-vne-swap.md @@ -0,0 +1,7 @@ +# Панель действий ревью вне htmx-свопа блока источника + +**Приоритет:** низкий + +При выборе источника одним кликом обновляется только блок источника (#source-block) htmx-свопом, а нижняя панель действий (кнопка «Применить», завязанная на HasLinks) — вне блока и не обновляется до полной перезагрузки. Практически не мешает (хардлинки только по явному «Применить», Apply без плана вернёт ошибку), но в краевом случае (источник с пустым предпросмотром из-за коллизии) кнопка может остаться/пропасть не синхронно. Решение намечено в дизайне review-unified-source-block: обновлять панель hx-swap-oob из того же партиала. + +Связано: openspec/specs/review, openspec/specs/web-ui, пакет httpapi. diff --git a/docs/backlog/retention-ochistka-bd.md b/docs/backlog/retention-ochistka-bd.md new file mode 100644 index 0000000..f3eef1a --- /dev/null +++ b/docs/backlog/retention-ochistka-bd.md @@ -0,0 +1,7 @@ +# Ретеншн и очистка БД + +**Приоритет:** высокий + +Терминальные задачи (done/cancelled/failed/reverted), их попытки recognition с сырыми ответами LLM и metadata_candidate копятся вечно — БД и список загрузок распухают и становятся нечитаемыми. Нужна авточистка старше N дней (настройка в [storage] или [worker]) и/или ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере эксплуатации. + +Связано: specs/architecture.md → «Хранилище» (download/recognition/metadata_candidate/file_link), пакет store. diff --git a/docs/backlog/review-f1-gate-dozapisi-heshey.md b/docs/backlog/review-f1-gate-dozapisi-heshey.md new file mode 100644 index 0000000..4104c57 --- /dev/null +++ b/docs/backlog/review-f1-gate-dozapisi-heshey.md @@ -0,0 +1,13 @@ +# Гейт дозаписи хешей в dedup-ветке CreateDownloadIfNoActive (F1) + +**Приоритет:** высокий · **Теги:** ingest, review-2026-07-08, invariant + +Ревью Fable 2026-07-08 (приём). internal/store/download.go:271-290. + +Проблема: dedup-ветка CreateDownloadIfNoActive безусловно дописывает ВСЕ хеши norm в найденную активную задачу (INSERT OR IGNORE) без пер-хеш гарда владения — в отличие от AddInfohashes (download.go:382-418), у которого гард есть. Единственная неохраняемая запись хешей — в авторитетном методе инварианта. + +Сценарий: активная A владеет v1, активная B владеет v2 того же торрента (split-identity, см. F4) ИЛИ крафт-магнет (F5) → гибрид {v1,v2} дописывает v1 в B → две активные владеют v1. Инвариант «≤1 активная на infohash» нарушен. Спека ingest «Атомарность возврата в активное» это запрещает. + +Фикс: применить пер-хеш гард как в AddInfohashes (исключить existing.ID, пропускать хеши чужой активной задачи). Tx уже открыта. + +Вердикт: простой фикс (поведение уже обещано спекой). diff --git a/docs/backlog/review-f2-promote-without-add.md b/docs/backlog/review-f2-promote-without-add.md new file mode 100644 index 0000000..ac65f68 --- /dev/null +++ b/docs/backlog/review-f2-promote-without-add.md @@ -0,0 +1,13 @@ +# processCatched: promote-without-add если торрент уже в qBittorrent (F2) + +**Приоритет:** средний · **Теги:** ingest, review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (приём). worker.go:361-391, sourceAddParts :407-432, qbt.go:243-246. + +Сценарий: торрент уже в qBittorrent БЕЗ нашей категории/тега (юзер добавил вручную раньше → discover не усыновляет). Юзер грузит тот же .torrent в jellybit → catched → qbt.Add файлом; для file-add дубль → «Fails.» → Add ошибка → «will retry» каждый тик, вечно, до catch_timeout → failed/qbit_add, который reconcileRecovery НЕ воскрешает (worker.go:124-132). Торрент жив всё это время; юзер видит failed. Retry уже решает это alive-проверкой (worker.go:692-698 «повторный Add вреден»), а processCatched — нет, хотя live-снимок byHash того же тика доступен. Также лечит сценарий B (Add успех, PromoteCatched падает на транзиентной ошибке → снова Add дубля). + +Замечание: поведение qBit на дубль file-add версионно-зависимо («Fails.» vs «Ok.») — проверить на целевой версии. + +Фикс: перед Add проверить присутствие хешей в qBit; есть → promote без Add (зеркалит Retry). + +Вердикт: change (малая спека-дельта download-tracking + код). diff --git a/docs/backlog/review-f3-cancel-during-add.md b/docs/backlog/review-f3-cancel-during-add.md new file mode 100644 index 0000000..8080a53 --- /dev/null +++ b/docs/backlog/review-f3-cancel-during-add.md @@ -0,0 +1,11 @@ +# Cancel во время add оставляет неуправляемый торрент в qBittorrent (F3/NIT-13) + +**Приоритет:** средний · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (оба ревьюера: F3 + NIT-13). worker.go:368-390, discover.go:43-50. + +Сценарий: задача catched, worker вне w.mu выводит имя (LLM, секунды) + qbt.Add (успех). Параллельно user Cancel (catched→cancelled). PromoteCatched корректно пропускает (гард state='catched', спека соблюдена). НО торрент ДОБАВЛЕН в qBit под нашей категорией, будет качаться/сидировать вечно. Усыновить назад нельзя: adopt через ExistsByInfohash (любое состояние) → хеши cancelled-задачи существуют. Торрент ест диск без видимой задачи и владельца. Спека покрывает переход состояния, но не побочный эффект. Инвариант «источник неприкосновенен» — но этот торрент добавили МЫ после cancel-намерения. + +Фикс-опции: (a) re-read state прямо перед Add (сужает окно); (b) при promote-skip из-за cancel — WARN «torrent left in qBittorrent»; (c) scoped delete/pause только что добавленного нами. + +Вердикт: change (нужно решение по инварианту «источник неприкосновенен»). diff --git a/docs/backlog/review-f4-f5-infohash-identity.md b/docs/backlog/review-f4-f5-infohash-identity.md new file mode 100644 index 0000000..cde5da5 --- /dev/null +++ b/docs/backlog/review-f4-f5-infohash-identity.md @@ -0,0 +1,11 @@ +# Идентичность инфохэшей: split v1/v2 одного торрента + крафт-магнет отравляет владение (F4, F5) + +**Приоритет:** низкий · **Теги:** ingest, review-2026-07-08 + +Ревью Fable 2026-07-08 (приём). Две связанные находки о доверии к парам xt в magnet (предпосылки к F1). + +F4 — split identity: v1-only magnet гибридного торрента T → задача A (catched). v2-only magnet того же T → дедуп не находит (строки хешей не связаны) → задача B. Обе активны (инвариант пер-хеш, не пер-торрент). A добавляется; qBit раскрывает infohash_v1+v2; captureInfohashes(A) пытается добавить v2 → ErrInfohashTaken (владеет B) → WARN КАЖДЫЙ тик. Add B — дубль → «Fails.» цикл → failed/qbit_add. Итог eventually-consistent, но: ложный failed, часы WARN, при худшем — обе reconcile против одного торрента → двойное распознавание/раскладка. captureInfohashes УЗНАЁТ факт (ErrInfohashTaken несёт владельца), но выбрасывает в WARN. Фикс-минимум: дедуп/дебаунс WARN; лучше — решение merge/supersede. + +F5 — крафт-магнет: активная X владеет v1(X). Магнет с xt=btih:v1(X) + xt=btmh:v2(Y) где Y без активного владельца. Дедуп матчит X по v1; топ-ап пишет v2(Y) в X (гард отклоняет только хеши ЧУЖОЙ активной, у Y её нет). Теперь приём Y дедупит на X, Y никогда не качается до терминала X. CLAUDE.md трактует вывод LLM недоверенным, но ПАРУ полей magnet — доверяет. Транспорты semi-trusted (Telegram allowlist, LAN) → импакт низкий; но пересланное вредоносное сообщение трекер-бота — это ровно Telegram-поток. Фикс: топ-ап хешей на existing только когда qBit подтвердил пару (оставить топ-ап captureInfohashes, убрать из ingest attach). + +Вердикт: change (решение по модели доверия/идентичности) либо задокументировать как ограничение. Связано с F1. diff --git a/docs/backlog/review-f6-torrent-over-magnet.md b/docs/backlog/review-f6-torrent-over-magnet.md new file mode 100644 index 0000000..ae560a2 --- /dev/null +++ b/docs/backlog/review-f6-torrent-over-magnet.md @@ -0,0 +1,11 @@ +# .torrent поверх magnet при дедупе теряет байты — потерян upgrade-путь (F6) + +**Приоритет:** средний · **Теги:** ingest, review-2026-07-08 + +Ревью Fable 2026-07-08 (приём). ingest.go:85-90, download.go:252-253, спека ingest «при дедупликации байты сохраняться SHALL NOT». + +Сценарий: magnet с приватного трекера → catched, source_type=magnet. Юзер понимает, что magnet не докачает метаданные (нет DHT), грузит правильный .torrent. Ingest дедупит по infohash на magnet-задачу; по спеке блоб НЕ сохраняется, source_type остаётся magnet. Worker добавляет по magnet-URL → metaDL вечно → failed/magnet_timeout через 24ч. Юзер дал именно артефакт, который бы починил, — выброшен с «уже в работе». Retry снова по magnet. Рационал самой спеки (хранить байты, «иначе на закрытых трекерах не докачать») спорит с её же правилом дедупа здесь. + +Фикс: при дедупе, где входящее — torrent-байты, а existing — catched с source_type=magnet: сохранить блоб и сменить source_type в той же tx. + +Вердикт: change (противоречит текущему предложению спеки, нужна дельта). diff --git a/docs/backlog/review-f7-f10-ingest-ui-fixes.md b/docs/backlog/review-f7-f10-ingest-ui-fixes.md new file mode 100644 index 0000000..ab40783 --- /dev/null +++ b/docs/backlog/review-f7-f10-ingest-ui-fixes.md @@ -0,0 +1,17 @@ +# Приём/UI: мелкие фиксы границ и парсинга (F7, F8, F9, F10, N2) + +**Приоритет:** низкий · **Теги:** ingest, review-2026-07-08 + +Ревью Fable 2026-07-08 (приём). Пачка независимых простых фиксов. + +F7 — oversized .torrent через веб → 500 вместо 400. ingest.go:148-151 отдаёт plain fmt.Errorf «torrent too large», classifyErr (httpapi.go:753-770) → default → 500. Web MaxBytesReader пропускает MaxTorrentSize+1MiB. Фикс: sentinel-ошибка размера → 400. (Telegram ок — pre-check doc.FileSize.) + +F8 — fast-path attach гонка с cancel. ingest.go:85-90,205-217: FindActiveByInfohash (без tx) вернул catched, параллельно cancel → attached() отдаёт Deduplicated=true (stale «уже в работе»), ничего не активно. Фикс: убрать ранний return, дедуп-решение только в CreateDownloadIfNoActive (уже re-check под BEGIN IMMEDIATE). + +F9 — magnet parsing. magnet.go:46-51: HasPrefix «urn:btih:» регистрозависим → magnet:?xt=URN:BTIH:… отклоняется (RFC 2141: URN регистронезависим; scheme уже матчится EqualFold :91). tgbot/parse.go:11: magnet:\?[^\s]+ приклеивает хвостовую пунктуацию (точка/скобка) → xt последним → длина 41 → отказ. Фикс: case-insensitive префикс; trim хвостовых .,;:)]}>» в ParseMessage. + +F10 — cap размера context из веб-формы. httpapi.go:405,412-414: multipart-бюджет на всё тело; без файла поле context ~9MiB → download.context в БД, рендер, LLM-промпты. REST 64KiB, Telegram лимит подписи — открыт только web. Фикс: cap Context в Ingest (одно место) ~16KiB с маркером. + +N2 — shorten() режет по байтам не рунам (httpapi.go:713-718): кириллические source_ref-заголовки (частый случай) рвутся посреди руны → U+FFFD. Фикс: рунобезопасная обрезка. + +Вердикт: все простые фиксы. diff --git a/docs/backlog/review-ingest-nits.md b/docs/backlog/review-ingest-nits.md new file mode 100644 index 0000000..543c67c --- /dev/null +++ b/docs/backlog/review-ingest-nits.md @@ -0,0 +1,15 @@ +# Нити приёма: NoName в контексте, устаревшие комментарии, лог без причины, bencode-аллокации (N1, N3, N4, N5) + +**Приоритет:** низкий · **Теги:** ingest, review-2026-07-08 + +Ревью Fable 2026-07-08 (приём). Косметические нити. + +N1 — torrent.go:114: Context() включает NoName-сентинел «-» как строку-название (name != "" проходит); ingest.parse фильтрует «-» только для source_ref (ingest.go:160-162). Одна грязная строка контекста для безымянных торрентов. Фикс: фильтровать «-» и в Context(). + +N3 — устаревшие комментарии. httpapi.go:574-577 и tgbot/bot.go:256-258 утверждают, что res.DownloadID может быть непуст при ошибке приёма («сбой после создания задачи, напр. qbit»). После fast-catch рефактора Ingest возвращает Result{} на КАЖДОМ пути ошибки (ingest.go:74-75,86,106-107) → корреляция всегда падает на request_id / без ключа. Фикс: поправить комментарии. + +N4 — qbt.go:246 логирует «Fails.» со счётчиками, но qBittorrent не даёт причину; вместе с F2 оператор не отличит «дубль» от «битый файл». Идея: логировать хеши/первые байты для корреляции. + +N5 — anacrolix bencode (v1.61.0, bencode/decode.go:17,250) аллоцирует до MaxStrLen (~128MiB) на объявленную строку до чтения — крафт-8MiB-торрент может форсить транзиентные ~128MiB аллокации при metainfo.Load. Ограничено и завершается ошибкой; на umbar приемлемо, но знать стоит. (files()-panic-guard torrent.go НЕ покрывает Load/UnmarshalInfo/HashBytes, но panic-путей там не найдено.) + +Вердикт: простые фиксы/принять. diff --git a/docs/backlog/review-lifecycle-minor.md b/docs/backlog/review-lifecycle-minor.md new file mode 100644 index 0000000..3559c11 --- /dev/null +++ b/docs/backlog/review-lifecycle-minor.md @@ -0,0 +1,15 @@ +# Жизненный цикл: мелкие находки (MINOR-8, MINOR-9, NIT-11, NIT-12) + +**Приоритет:** низкий · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (жизненный цикл). Мелкие находки. + +MINOR-8 — recognition claim-token отсутствует. review.go:160-177: задача в recognizing, LLM в полёте (вне w.mu); user Cancel→Relink (recognizing→cancelled→recognizing revive). Старый вызов завершается, finishRecognition re-check d.State==recognizing — true, но это НОВЫЙ claim → устаревший результат коммитится (recognizing→review); свежий прогон отбрасывает себя. Импакт низкий (входы почти идентичны), но потерянный LLM-вызов + результат приписан не той попытке. Фикс: claim-token (updated_at на момент claim, или id строки recognition). + +MINOR-9 — I/O под глобальным w.mu. worker.go:677-734 (Retry: torrentByInfohash + qbt.Add под mu), review.go: ensureSourcePresent (сеть) в Relink/Rerecognize/Refine/SetType, Apply (torrentByInfohash + layouter.Apply FS I/O), Undo (FS I/O) — держат w.mu целиком. Нарушает своё же правило «медленные вызовы вне блокировки» (processCatched/recognizeOne его соблюдают). Медленный/зависший qBit → каждый клик ревью = глобальный стопор поллинга и всех команд транспортов. Корректность ок (lock даёт race-free preflight), только liveness. Для one-user home-сервера — можно осознанно waive; худший — Retry с qbt.Add под mu при зависшем qBit. Фикс если делать: probe-вне-lock + re-validate-под-lock (как processCatched). + +NIT-11 — byHash индексирует и усечённый 40-hex t.Hash v2-only торрентов (worker.go:444-452). Хранение усечённых хешей отклоняется (discover.go:98-111), но lookup-мапа всё ещё ключует t.Hash → v1-хеш одной задачи теоретически == усечённый-v2 t.Hash другого торрента → матч не тому. Требует 160-бит коллизию (космологические шансы). Фикс: исключить t.Hash из мапы, когда есть infohash_v1/v2 (зеркалить torrentHashes). + +NIT-12 — Retry на failed/qbit_error (missingFiles) с живым errored-торрентом: alive=true → без re-Add → downloading → следующий тик classErrored → снова failed (+ дебаунс уведомления). Честно, но user-hostile: retry выглядит сломанным, реальное лекарство (recheck/fix в qBit) не подсказано. Фикс: отклонять retry (или форсить recheck) когда живой торрент classErrored. (Связано с задачей retry/stall семантики.) + +Вердикт: MINOR-8/NIT-11/NIT-12 — простые фиксы; MINOR-9 — change или осознанный waive. diff --git a/docs/backlog/review-major1-2-retry-stall.md b/docs/backlog/review-major1-2-retry-stall.md new file mode 100644 index 0000000..450d45e --- /dev/null +++ b/docs/backlog/review-major1-2-retry-stall.md @@ -0,0 +1,13 @@ +# Retry/stall семантика: сброс базиса таймаута + простой от начала, а не от возраста торрента (MAJOR-1, MAJOR-2) + +**Приоритет:** высокий · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (жизненный цикл). Два связанных бага. worker.go:677-734 (Retry), :514-525 (checkTimeouts), :578-592 (torrentAge). + +MAJOR-1: Retry с живым торрентом (alive=true) не переиздаёт Add, только ActivateIfNoOtherActive→downloading; базис age=now−added_on НЕ сбрасывается → следующий тик: stalledDL && age>StuckAfter → снова stuck (~5с). Спека state-reconciliation «Ручной повтор» требует: базис SHALL сбрасываться. Комментарий worker.go:690-691 верен лишь для re-Add ветки. Тест TestRetryReattaches не гоняет следующий тик. + +MAJOR-2: stuck_after меряет ВОЗРАСТ торрента (от added_on), а не длительность простоя. Торрент, качавшийся 5ч, при мгновенном stalledDL на 1 тик → stuck с сообщением «stalled for 5h» (ложь) + EventFailed. Проход через stalledDL между пирами — норма → флап stuck↔downloading + до-часовые ложные уведомления. Спека сама противоречива («stalledDL дольше stuck_after» vs «возраст от added_on»). + +Фикс: колонка retried_at и/или stalled_since (или qBit last_activity); базис = max(added_on, retried_at); простой мерить от stalled_since. Схема + миграция + сверка спеки. Покрывает также NIT-10 (фолбек added_on→created_at) и NIT-12 (retry на qbit_error мгновенно откатывается). + +Вердикт: полноценный change (схема + спека). Бьёт по повседневным сценариям — retry выглядит сломанным, длинные загрузки спонтанно флапают в stuck. diff --git a/docs/backlog/review-major3-zombie-downloading.md b/docs/backlog/review-major3-zombie-downloading.md new file mode 100644 index 0000000..f62d2e8 --- /dev/null +++ b/docs/backlog/review-major3-zombie-downloading.md @@ -0,0 +1,11 @@ +# Восстановление zombie downloading при пропаже источника из qBittorrent (MAJOR-3) + +**Приоритет:** высокий · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (жизненный цикл). worker.go:472-477. Подтверждено чтением кода. + +Сценарий: торрент удалён из qBittorrent (юзером/другим клиентом), пока задача в downloading. Poll: torrentFor промах → Warn «active download not found in qbittorrent» → continue. Каждый тик, вечно. reconcileDesync покрывает только done/target_missing/orphaned; reconcileRecovery — failed/stuck; дебаунса для этого случая НЕТ, состояние не меняется, уведомления нет, checkTimeouts требует торрент. Задача — вечный зомби, активна в UI без телеметрии; выход только Cancel/Defer. Тот же зомби при провале отката Retry (worker.go:716-729). Спека намеренно исключает активные из матрицы source×target, но «источник исчез в downloading» не владеет НИКТО — дыра спеки (сравн.: та же пропажа в completed/recognizing деградирует штатно). + +Фикс: расширить дебаунс пропажи источника (SourceMissCount) на downloading → после порога downloading→deleted (или failed с distinct error_code для re-Add) + уведомление. Нужно ребро графа. + +Вердикт: полноценный change. Классический «застрявшее состояние, которое никто не двигает». diff --git a/docs/backlog/review-major4-sweep-linking.md b/docs/backlog/review-major4-sweep-linking.md new file mode 100644 index 0000000..ed7678d --- /dev/null +++ b/docs/backlog/review-major4-sweep-linking.md @@ -0,0 +1,13 @@ +# Sweep застрявших linking при рестарте + фикс persist-failure (MAJOR-4) + +**Приоритет:** средний · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (жизненный цикл). review.go:284-287. + +(A) Без краха: linkPlan создаёт хардлинки на FS, затем CreateFileLinks падает (транзиентная ошибка SQLite) → return без перехода → задача в linking, хардлинки на диске без file_link-строк (Undo нечего откатывать, targetPresent=false). + +(B) Краш процесса между transition(StateLinking) (review.go:251) и финальным переходом → на рестарте linking не листит НИКТО (processCatched=catched, Poll=downloading, recognizePending=completed/recognizing, desync=done/tm/orphaned, recovery=failed/stuck). Задача сидит в linking вечно; выход только ручной Cancel/Defer (недискаверабельно). recognizing получил restart-healing (recognizePending), linking — нет — нарушен инвариант «у каждого нетерминального состояния есть владелец». + +Фикс: на тике/старте sweep linking-задач (любая под w.mu — по построению устаревшая) → linking→review (ребро есть) с error_msg «прерванная раскладка, повтори»; при persist-failure переходить в review/failed, а не bare-return. + +Вердикт: простой фикс (+ 1 спека-сценарий). diff --git a/docs/backlog/review-major5-readiness-preflight.md b/docs/backlog/review-major5-readiness-preflight.md new file mode 100644 index 0000000..e159836 --- /dev/null +++ b/docs/backlog/review-major5-readiness-preflight.md @@ -0,0 +1,11 @@ +# Readiness-preflight: запрет авто-раскладки недокачанных файлов (MAJOR-5) + +**Приоритет:** высокий · **Теги:** review-2026-07-08, lifecycle, data-integrity + +Ревью Fable 2026-07-08 (жизненный цикл). САМАЯ ОПАСНАЯ — целостность библиотеки. internal/worker/review.go (ensureSourcePresent проверяет присутствие источника, НЕ classify(t.State)==classReady). + +Сценарий: задача на 40% в downloading → user «Позже» (deferred, легально: граф допускает *→deferred) → «Распознать заново» (Rerecognize) → ensureSourcePresent проходит (торрент есть) → recognizing → LLM видит имена файлов (qBit отдаёт до завершения) → при уверенном матче res.Decision.Auto && !forceReview (Rerecognize НЕ ставит force_review, только Relink ставит) → авто linking → хардлинки на НЕДОКАЧАННЫЕ файлы → done → Jellyfin сканирует половину. Даже ручной review→Apply не имеет preflight завершённости. Обходит состояние completed, чей смысл (download-tracking «Готовность только когда файлы на месте») — финальность. + +Фикс: ensureSourceReady требует classify(t.State)==classReady для Rerecognize/Refine/SetType/Apply; иначе ErrConflict «торрент ещё качается». + +Вердикт: полноценный change (новое требование preflight на readiness). diff --git a/docs/backlog/review-major6-defer-catched.md b/docs/backlog/review-major6-defer-catched.md new file mode 100644 index 0000000..1c6f493 --- /dev/null +++ b/docs/backlog/review-major6-defer-catched.md @@ -0,0 +1,11 @@ +# Defer из catched → лимбо → необратимый deleted (MAJOR-6) + +**Приоритет:** средний · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (жизненный цикл). review.go:465-479, worker.go:332-340, reconcile.go:251-265. + +Сценарий: задача в catched (ещё не добавлена в qBit) → user Defer → deferred. processCatched больше её не видит (листит только catched) → торрент никогда не добавится. Из deferred: Apply→«нет плана», Rerecognize/Refine→ensureSourcePresent нет торрента→reconcileToReality(sourcePresent=false, targetPresent=false)→deriveState=deleted, а у deleted НОЛЬ исходящих рёбер (download.go:102) → задача необратима, хотя байты .torrent лежат в download_torrent. Также deleted семантически неверен (ничего не качалось/раскладывалось). + +Фикс: исключить catched из Defer (пре-источниковое состояние, «позже» бессмысленно) ИЛИ processCatched резюмит deferred-без-recognition ИЛИ preflight «источника не было никогда» (source_added_at IS NULL и нет links) → failed/qbit_add (retriable), не deleted. + +Вердикт: простой фикс (исключить catched из Defer). diff --git a/docs/backlog/review-minor7-transition-errors.md b/docs/backlog/review-minor7-transition-errors.md new file mode 100644 index 0000000..4bad547 --- /dev/null +++ b/docs/backlog/review-minor7-transition-errors.md @@ -0,0 +1,11 @@ +# transition() глотает ошибки перед созданием хардлинков (MINOR-7) + +**Приоритет:** средний · **Теги:** review-2026-07-08, lifecycle + +Ревью Fable 2026-07-08 (жизненный цикл). worker.go:595-601 (ошибка логируется, не возвращается), review.go:251-252, :196-198. + +Сценарий: в Apply w.transition(StateLinking) на транзиентной ошибке БД → залогировано, выполнение продолжается → linkPlan создаёт хардлинки, пока задача ещё в review. Финальная запись linking→done оценивается как review→done — НЕ в графе → отклонена → файлы на диске, задача застряла в review со stale-планом; file_link-строки есть (re-Apply увидит StatusExists, частично самолечится), но done не достигнут, скан/уведомление не сработали. Паттерн claim-then-side-effect корректен только если claim проверяется (везде ещё — PromoteCatched, finishRecognition — гейтят; тут нет). + +Фикс: transition возвращает ошибку (или mustTransition); Apply/finishRecognition прерываются до linkPlan при провале claim. + +Вердикт: простой фикс. diff --git a/docs/backlog/sila-sovpadeniya-kandidata.md b/docs/backlog/sila-sovpadeniya-kandidata.md new file mode 100644 index 0000000..c3ca3da --- /dev/null +++ b/docs/backlog/sila-sovpadeniya-kandidata.md @@ -0,0 +1,7 @@ +# [идея] Сила совпадения кандидата и пересмотр распознавания/матчинга + +**Приоритет:** средний + +ИДЕЯ (сперва проработать). У кандидата метабазы нет метрики силы совпадения (metadata_candidate хранит provider/id/title/year/url), решение «авто vs review» — по правилу «единственный сильный матч + валидация», не по числу. Для ревью: список кандидатов нечем отсортировать/подсветить по уверенности. Идея — ввести на этапе матча силу совпадения кандидата (точное совпадение названия+года vs частичное) для сортировки и подсказки в UI. Шире — продумать сам процесс распознавания и матчинга: границы «разбор LLM / поиск в базе / сверка», что храним у кандидата, как считаем и показываем уверенность. + +Связано: specs/recognition.md, ADR-2026-06-13-auto-link-requires-db-match, specs/review-ux.md. diff --git a/docs/backlog/slozhnye-serialnye-razdachi.md b/docs/backlog/slozhnye-serialnye-razdachi.md new file mode 100644 index 0000000..1695c73 --- /dev/null +++ b/docs/backlog/slozhnye-serialnye-razdachi.md @@ -0,0 +1,7 @@ +# [идея] Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки + +**Приоритет:** средний + +ИДЕЯ (проработать крайние случаи). Обычный случай — один сезон (его номер видно глазами и сверяем на ревью — под это сделана сводка сезонов). Но в редких заказах раздача сложнее: все сезоны сериала разом, пак нескольких сезонов, смешанная нумерация, вложенные папки сезонов, разнобойные имена файлов. Сейчас PlanFile.Season задаётся на каждом файле (мультисезон в принципе выразим), но целостно эти сценарии не проработаны: как надёжно распознать, как показать на ревью, как разложить и как стыкуется со сходимостью папки и merge-докачиванием. Решить, что поддерживаем явно, а что уводим в ревью как «сложную раскладку». + +Связано: specs/recognition.md, specs/jellyfin-layout.md, specs/review-ux.md, «Проблема второго сезона», «Раздачи с докачиванием». diff --git a/docs/backlog/sse-obnovleniya.md b/docs/backlog/sse-obnovleniya.md new file mode 100644 index 0000000..e125a55 --- /dev/null +++ b/docs/backlog/sse-obnovleniya.md @@ -0,0 +1,7 @@ +# Мгновенные обновления через SSE + +**Приоритет:** низкий + +Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает, но с задержкой в интервал опроса и холостыми запросами. Перевести динамический контент (прогресс загрузки, смена статуса, раздача) на Server-Sent Events, чтобы обновления приходили почти мгновенно и без лишнего поллинга. Поллинг работает, поэтому это улучшение, а не блокер; SSE — один долгоживущий ответ на соединение, ложится на server-rendered UI без тяжёлого фронтенда. + +Связано: specs/architecture.md → «Транспорты», specs/review-ux.md, пакет httpapi. diff --git a/docs/backlog/svobodnoe-mesto-copy-fallback.md b/docs/backlog/svobodnoe-mesto-copy-fallback.md new file mode 100644 index 0000000..e7c5b0c --- /dev/null +++ b/docs/backlog/svobodnoe-mesto-copy-fallback.md @@ -0,0 +1,7 @@ +# Проверка свободного места перед copy-fallback + +**Приоритет:** низкий + +Когда хардлинк невозможен (EXDEV/ENOTSUP/…), layout копирует файл, дублируя место на диске. На забитом диске это упрётся в полку посреди раскладки. Перед копированием проверять доступное место и при нехватке внятно уходить в failed с понятной причиной, а не падать на полпути. + +Связано: specs/architecture.md → «Раскладка файлов» (фолбэк-копирование), пакет layout. diff --git a/docs/backlog/telegram-match-metabazy.md b/docs/backlog/telegram-match-metabazy.md new file mode 100644 index 0000000..f7c8ecb --- /dev/null +++ b/docs/backlog/telegram-match-metabazy.md @@ -0,0 +1,7 @@ +# Улучшения UI: показывать матч с записью метабазы в Telegram + +**Приоритет:** средний + +Web-сторона реализована: страница загрузки /download/{id} и экран ревью показывают, с какой именно записью метабазы (TMDB/TVDB/IMDb) сматчилась загрузка — провайдер, id и ссылку. Осталось довести то же в Telegram: в уведомлениях/подтверждениях показывать запись матча (название, год, провайдер-id, ссылку), чтобы ошибочную привязку было видно и из бота. Полный выбор источника в вебе уже реализован. + +Связано: specs/review-ux.md, specs/recognition.md (матч в базе), specs/architecture.md → «Транспорты». diff --git a/docs/backlog/telegram-vybor-nahodok.md b/docs/backlog/telegram-vybor-nahodok.md new file mode 100644 index 0000000..5f7db0b --- /dev/null +++ b/docs/backlog/telegram-vybor-nahodok.md @@ -0,0 +1,7 @@ +# Выбор из нескольких находок метабазы в Telegram + +**Приоритет:** низкий + +Когда распознавание даёт несколько подходящих кандидатов в метабазе, предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча брать первый/лучший. Веб остаётся точкой точных правок (полный выбор источника уже реализован), бот — быстрый выбор из готового короткого списка. + +Связано: specs/review-ux.md (боты — быстрые действия, веб — точные правки), specs/recognition.md (кандидаты матча). diff --git a/docs/backlog/ubiquitous-language-slovar.md b/docs/backlog/ubiquitous-language-slovar.md new file mode 100644 index 0000000..da233c8 --- /dev/null +++ b/docs/backlog/ubiquitous-language-slovar.md @@ -0,0 +1,7 @@ +# Словарь единого языка (ubiquitous language) + +**Приоритет:** средний + +Свести термины домена в один глоссарий, чтобы пользователь, документация, код и агент говорили на одном языке: загрузка, раздача, распознавание, матч, кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и т.д. — русский термин, английский идентификатор в коде, краткое определение. Сейчас наименования расходятся между спеками, UI и кодом. Глоссарий — источник истины по именам; на нём же строится агент-ревьювер наименований. + +Связано: docs/conventions, specs/architecture.md, новый файл-глоссарий. diff --git a/docs/backlog/udalenie-edinoe-okno.md b/docs/backlog/udalenie-edinoe-okno.md new file mode 100644 index 0000000..7cc48e0 --- /dev/null +++ b/docs/backlog/udalenie-edinoe-okno.md @@ -0,0 +1,13 @@ +# Удаление средствами jellybit («единое окно», path 2) + +**Приоритет:** высокий + +Распознавание ручного удаления и пометка рассинхрона уже сделаны (state-reconciliation: target_missing/orphaned/deleted, безопасный undo с nlink-гардом, preflight). Осталось (path 2) — удалять просмотренное из самого jellybit, не идя руками в qBittorrent/Jellyfin. «Тайтл» — вычисляемая группа загрузок по (provider, provider_id)/общей папке, без новой сущности; удаление целиком — обход загрузок группы штатным undo; удаление раздачи из qBittorrent — осознанный выход за инвариант «источник неприкосновенен», только по явному подтверждению. + +Шаги: +- удаление одной загрузки: снять живые хардлинки (штатный undo, superseded пропускаем, nlink-гард) + опц. удалить раздачу из qBittorrent с файлами — с подтверждением +- вычисляемая группа «тайтл» в UI: состав сериала/фильма одним экраном +- удаление тайтла целиком: обход загрузок группы + опц. снос опустевшей папки +- после полного удаления память о тайтле не остаётся + +Связано: drafts/logical-title-model.md §5.3/§6.4, ADR-2026-06-13-hardlinks, specs/architecture.md, specs/workflow.md diff --git a/docs/backlog/uvedomleniya-multi-bot.md b/docs/backlog/uvedomleniya-multi-bot.md new file mode 100644 index 0000000..3632c79 --- /dev/null +++ b/docs/backlog/uvedomleniya-multi-bot.md @@ -0,0 +1,7 @@ +# Привязка уведомлений к источнику в ботах (мульти-бот) + +**Приоритет:** средний + +Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней. Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся единым для всех и точкой правды по функциональности (боты — тонкие адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и идентификатор отправителя, маршрутизировать пинги по нему. + +Связано: specs/review-ux.md, specs/architecture.md → «Транспорты». diff --git a/docs/backlog/versii-kachestvo-repaki.md b/docs/backlog/versii-kachestvo-repaki.md new file mode 100644 index 0000000..631672c --- /dev/null +++ b/docs/backlog/versii-kachestvo-repaki.md @@ -0,0 +1,7 @@ +# Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p) + +**Приоритет:** низкий + +По калибровке болей (2026-07-02) — не боль, из приоритета выпало. Сосуществование версий доступно уже сейчас (Jellyfin multi-version, другой целевой путь), коллизия на тот же путь штатно уходит в review. Явный replace (undo старого хардлинка → lay нового → супересид владения путём) — отдельный change, если/когда станет болью. + +Связано: «Раздачи с докачиванием», specs/jellyfin-layout.md (never-overwrite, коллизия). diff --git a/docs/backlog/vneshnie-subtitry.md b/docs/backlog/vneshnie-subtitry.md new file mode 100644 index 0000000..23ad76a --- /dev/null +++ b/docs/backlog/vneshnie-subtitry.md @@ -0,0 +1,7 @@ +# Привязка внешних субтитров к серии (сериалы) + +**Приоритет:** средний + +Аудит спек↔код (2026-07-03): спека recognition требует «внешние субтитры SHALL привязываться к соответствующему видео». Для фильма работает — раскладка именует субтитр по базе видеофайла. Для сериала связь субтитр→конкретная серия не выражена: в PlanFile нет поля привязки, и нет логики спаривания VobSub .idx+.sub. Смоделировать привязку субтитра к эпизоду (поле на PlanFile или роль с season/episode) и спаривание .idx+.sub, либо — если откладываем — сузить формулировку спеки до реального поведения. + +Связано: openspec/specs/recognition, specs/jellyfin-layout.md, пакеты recognize, layout. diff --git a/docs/backlog/vtoroy-sezon-shodimost-papki.md b/docs/backlog/vtoroy-sezon-shodimost-papki.md new file mode 100644 index 0000000..1383ddd --- /dev/null +++ b/docs/backlog/vtoroy-sezon-shodimost-papki.md @@ -0,0 +1,13 @@ +# Проблема второго сезона (сходимость папки сериала) + +**Приоритет:** высокий + +Второй/третий сезон должен ложиться в ТУ ЖЕ папку сериала, а не заводить рядом почти одинаковую. Проблема не в группировке, а в сходимости папки: имя печатается заново из выхода 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, specs/recognition.md, specs/jellyfin-layout.md diff --git a/docs/backlog/web-ui-pwa.md b/docs/backlog/web-ui-pwa.md new file mode 100644 index 0000000..a3d5941 --- /dev/null +++ b/docs/backlog/web-ui-pwa.md @@ -0,0 +1,7 @@ +# Современный Web-UI как PWA + +**Приоритет:** низкий + +Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое, удобное с телефона). Текущий server-rendered UI функционален, поэтому это улучшение, а не блокер; большой объём работы. + +Связано: specs/review-ux.md (веб = точные правки), пакет httpapi. diff --git a/docs/backlog/webhook-zavershenie-zagruzki.md b/docs/backlog/webhook-zavershenie-zagruzki.md new file mode 100644 index 0000000..a0dceb2 --- /dev/null +++ b/docs/backlog/webhook-zavershenie-zagruzki.md @@ -0,0 +1,7 @@ +# [идея] Завершение загрузки через webhook + +**Приоритет:** низкий + +ИДЕЯ (решим по опыту эксплуатации). Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд. Альтернатива: «Run external program on torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом qBittorrent. + +Связано: specs/architecture.md → «Отслеживание загрузки», пакет worker. diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md index b92c096..487d546 100644 --- a/docs/specs/architecture.md +++ b/docs/specs/architecture.md @@ -61,8 +61,8 @@ reject / defer / undo) — команды к `worker`: (server-rendered). В v1 **без авторизации** (доверенная LAN). Поле `http.trusted_subnets` зарезервировано, но **пока не применяется**: деплой только в локальную сеть без доступа из интернета, поэтому - allowlist-middleware и авторизацию отложили — задача «Авторизация - веб-UI» в беклоге (Tududi, проект «jellybit»). + allowlist-middleware и авторизацию отложили — задача + [«Авторизация веб-UI»](../backlog/avtorizaciya-web-ui.md) в беклоге. - **Telegram-бот** — переслать magnet/сообщение бота; текст становится контекстом. Доступ — по `telegram.allowed_user_ids` (пусто = запрет всем, fail-closed). Бот же шлёт **пинги** о входе в review/готовности. diff --git a/docs/specs/jellyfin-layout.md b/docs/specs/jellyfin-layout.md index ddf048e..fc4629e 100644 --- a/docs/specs/jellyfin-layout.md +++ b/docs/specs/jellyfin-layout.md @@ -99,4 +99,4 @@ inode общий — диск не дублируется. сезонам. - **Несколько аудиодорожек** — обычно внутри mkv, не наша забота. - **Аниме с абсолютной нумерацией** — пересчёт в S·E, отдельная проработка - (задача в беклоге — Tududi, проект «jellybit»). + ([задача в беклоге](../backlog/anime-absolyutnaya-numeraciya.md)). diff --git a/docs/specs/recognition.md b/docs/specs/recognition.md index 430f577..7c102fe 100644 --- a/docs/specs/recognition.md +++ b/docs/specs/recognition.md @@ -133,12 +133,12 @@ notes пояснения, неоднозначности - Сезон-паки разбираем по сериям; смешанные паки, спецвыпуски (`Season 00`), двойные серии (`SxxEyy-Eyy`) — через per-file season/episode; любая неоднозначность → review. -- Аниме с абсолютной нумерацией — отдельный крайний случай, задача - в беклоге (Tududi, проект «jellybit»). +- Аниме с абсолютной нумерацией — отдельный крайний случай, + [задача в беклоге](../backlog/anime-absolyutnaya-numeraciya.md). ## На будущее `go-ptn` слабее питоновского `guessit`. Если точности пред-парса не хватит — завернуть `guessit` лёгким сервисом-спутником (один файл рядом с -бинарём). Задача «guessit как сервис-спутник» в беклоге (Tududi, проект -«jellybit»). +бинарём). Задача [«guessit как сервис-спутник»](../backlog/guessit-sputnik.md) +в беклоге. diff --git a/docs/specs/review-ux.md b/docs/specs/review-ux.md index d8141f1..eaf2c22 100644 --- a/docs/specs/review-ux.md +++ b/docs/specs/review-ux.md @@ -119,8 +119,8 @@ Telegram = одобрить / подсказать / выбрать кандид - **База неоднозначна** → выбор кандидата (часто чинит всё разом: пиннит provider-id и каноническое имя). - **База пустая (рус/аниме)** → «без базы» или ручной id/url. Аниме с - абсолютной нумерацией → веб-хелпер «absolute → S·E» (задача - «Аниме с абсолютной нумерацией» в беклоге — Tududi, проект «jellybit»). + абсолютной нумерацией → веб-хелпер «absolute → S·E» + ([задача «Аниме с абсолютной нумерацией»](../backlog/anime-absolyutnaya-numeraciya.md)). - **Не тот тип (movie↔series)** → переключатель пересобирает форму плана. - **Мусор (sample/extra/дубли дорожек)** → роль «игнор». - **Полный провал** (LLM ничего не вытащил) → веб-«ручной режим»: выбрать