docs: канон поднят с версии 7 до 12

- каталог задач переехал в tasks/ в корне, спринт упразднён — приоритет
  теперь порядок строк в BACKLOG.md, четыре задачи набора вернулись в беклог
- гейт: путь docs.py переведён на av-dev-docs вместо снесённого av-dev-pm,
  добавлены шаги tasks.py check и openspec.py check
- относительные ссылки внутри задач и ссылки из docs/ на задачи починены
This commit is contained in:
av
2026-08-09 19:09:53 +03:00
parent 9a624d4e13
commit c5d62d76ee
54 changed files with 109 additions and 79 deletions
+52
View File
@@ -0,0 +1,52 @@
# Беклог
Что **можно взять**. Одна задача = один файл `items/<slug>.md`
+ строка здесь. Целей тут нет — они в [ROADMAP.md](ROADMAP.md): беклог — то, что
берут, роадмап — то, подо что берут. Порядка внутри секции нет: «что делать
дальше» отвечает набор спринта. Ведётся скиллом `tasks`.
Секции «блокеры» здесь нет и не заводится: блокер — это состояние
(спринт не может продолжаться ни одной задачей), оно живёт до ответа
человека, а его следы — вопросами в файлах задач.
## Ядро продукта
- [✨ Пересчитывать абсолютную нумерацию аниме в SxxEyy](items/anime-absolute-numbering.md) — аниме со сквозной нумерацией (#137) не раскладывается в SxxEyy, который ждёт Jellyfin — нужен пересчёт абсолютной нумерации
- [✨ Раскладывать раздачу-копию диска (VIDEO_TS/BDMV) каталогом целиком](items/disc-image-releases.md) — раздача-образ диска (VIDEO_TS/BDMV) сейчас разбирается пофайлово вместо раскладки каталога целиком — редкий, но реальный случай
- [✨ Принимать ссылку на .torrent и скачивать файл самим (нужен SSRF-гард)](items/torrent-url-fetch.md) — magnet и .torrent-файл приняты; остался фетч .torrent по URL (нужен SSRF-гард)
- [✨ Докладывать недостающие эпизоды merge-раскладкой при повторной заливке](items/merge-incremental-redownload.md) — повторная заливка сериала целиком должна доложить недостающие эпизоды merge-раскладкой, не трогая существующие ссылки — блокирует типовой сценарий свежих сериалов
- [🐞 Связать v1/v2-хеши одного торрента и не доверять паре xt из магнета (F4, F5)](items/infohash-identity-integrity.md) — split v1/v2 идентичность и крафт-магнет отравляют владение инфохэшами _(ревью 2026-07-08)_
- [✨ Слать живые обновления через SSE вместо htmx-поллинга](items/sse-live-updates.md) — живые обновления на htmx-поллинге дают задержку и холостые запросы — SSE убрал бы то и другое (поллинг работает, поэтому улучшение, не блокер)
- [✨ Проверять свободное место перед copy-fallback](items/free-space-check-copy-fallback.md) — copy-fallback при невозможности хардлинка может упереться в переполненный диск посреди раскладки — нет проверки места до копирования
- [✨ Слать уведомления автору загрузки в его транспорт (мульти-бот)](items/notification-source-binding.md) — пинги и ревью должен получать автор загрузки в своём транспорте — нет привязки загрузки к источнику/отправителю (нужно для мульти-бота)
- [✨ Заменять разложенную версию тайтла новой (репаки, апгрейд 1080p → 2160p)](items/title-versions-repacks.md) — По калибровке болей (2026-07-02) — не боль, из приоритета выпало
- [✨ Спаривать VobSub .idx+.sub и проставлять языковой суффикс субтитра](items/external-subtitles.md) — Привязка субтитр→серия уже работает; остались пары VobSub .idx+.sub и потеря Lang/Flags
- [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
- [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом
- [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена
- [🔬 Форма ответа поиска TheTVDB и семантика параметра language](items/tvdb-search-response-live-check.md) — форма ответа поиска TheTVDB принята по swagger 4.7.10 и живым прогоном не подтверждена — при иной форме разбор молча уходит в фолбэк, гейт зелёный, локализованное название не работает
- [✨ Узаконить confidence-гейт авто-раскладки в спеке и сделать его выключаемым (дефолт 0.7)](items/auto-link-confidence-gate.md) — Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку
- [🐞 Отправлять на ревью раздачу с непомещающимся именем вместо отказа](items/long-title-to-review.md) — название длиннее ~237 байт роняет раскладку в failed с текстом системной ошибки: пользователь видит «file name too long» вместо карточки ревью, где это чинится подсказкой
- [🐞 Санитизировать название из метабазы перед подстановкой в план](items/metadata-title-sanitize.md) — plan.Title = match.Title подставляется ПОСЛЕ sanitizePlan — название из TMDB/TVDB уезжает в имя каталога Jellyfin дословно, с невидимыми символами и гомоглифами, и авто-раскладка это пропускает
- [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега
- [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
- [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём
- [🔬 Многоступенчатая верификация привязки](items/multi-pass-verification.md) — несколько проходов распознавания с консенсусом подняли бы точность ценой стоимости/латентности — проработать, когда включать и как мерджить расхождения
- [🔬 Сила совпадения кандидата и пересмотр распознавания/матчинга](items/candidate-match-strength.md) — у кандидата метабазы нет метрики силы совпадения — список кандидатов на ревью нечем отсортировать по уверенности (сперва проработать процесс матчинга)
- [🔬 Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки](items/complex-series-releases.md) — сложные раздачи (все сезоны разом, паки, смешанная нумерация) целостно не проработаны — распознавание/ревью/раскладка заточены под один сезон
## Инфраструктура
- [🧹 Откалибровать проходы ревью и завести ревьювер наименований](items/quality-review-agents.md) — конвейер ревью переехал в плагин `av-dev-pipeline`; осталась калибровка проходов на этом проекте и ревьювер наименований (ждёт словарь единого языка)
- [✨ Закрыть веб-UI авторизацией, когда доверенной LAN станет мало](items/web-ui-auth.md) — для v1 решено без авторизации (доверенная LAN, опц. allowlist подсетей) — задел на случай, если понадобится защита
- [🧹 Бэкапить SQLite по расписанию с ротацией](items/sqlite-backup.md) — architecture требует бекапить data-том, но стратегия не описана — сбой или редеплой стирают всё in-flight состояние (проще, пока БД маленькая)
- [✨ Проверять в healthcheck доступность qBittorrent, LLM и метабаз и показывать её в UI](items/deep-healthcheck-dependencies.md) — /healthz проверяет только сам сервис — недоступность qBittorrent/LLM/метабазы видна лишь по застрявшим задачам, нет readiness и бейджа в UI
- [✨ Хранить историю переходов загрузки отдельной таблицей](items/download-transition-history.md) — хранится только текущий статус загрузки — разбор «как сюда попали» идёт по логам сервера, нет таблицы истории переходов
- [🧹 Кэшировать ответы метабаз с TTL (и опционально LLM)](items/metadata-cache.md) — повторные и ретраящиеся прогоны бьют TMDB/TVDB/TVMaze одним запросом — кэш с TTL сэкономил бы лимиты и ускорил «Распознать заново»
- [✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения](items/db-retention-cleanup.md) — терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации
- [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
- [🧹 Разобрать кандидатов по тестам и записать конвенцию](items/tests-convention.md) — как пишем тесты, не записано нигде: пункт «Тесты» в convention-candidates не пересматривали, он обещает фикстуры в testdata/, которых в проекте нет — а трение накопилось (четыре внешних клиента, fakeStore с инъекцией ошибок, env-гейты, флаки-прогон, diff-coverage)
- [🐞 Тормозить опрос qBittorrent бэкоффом при недоступности и эскалировать устойчивый сбой](items/background-error-noise.md) — недоступный qBittorrent опрашивается каждые 5 с и даёт WARN на каждом тике: нужен экспоненциальный бэкофф до минутного потолка со сбросом по первому успеху и ERROR на устойчивой деградации
- [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
- [🔬 Завершение загрузки через webhook](items/completion-webhook.md) — завершение сейчас ловим поллингом qBittorrent — webhook реагировал бы быстрее, но связывает нас с его конфигом (решим по опыту эксплуатации)
- [🔬 Кандидаты в конвенции кода](items/convention-candidates.md) — накоплен список кандидатов (внешние клиенты, конкурентность, тесты, CLI, время) — надо решить, что из них стало реальным трением, а что выдумано вперёд
+9
View File
@@ -0,0 +1,9 @@
# Ушедшее без реализации
Задачи, покинувшие беклог **без реализации**, с причиной и датой.
Пишется `tasks.py close --reason`. Реализованные сюда не идут — у них
есть коммит. Это первое место, куда смотрит дедупликация при заведении.
<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Была секция: … -->
- 2026-08-06 `learn-from-user-corrections` — ✨ Подмешивать прошлые правки человека в промпт распознавания (few-shot). Причина: решено не собирать базу правок человека: направление — тюнинг автоматического распознавания без участия человека. Была секция: ядро продукта.
- 2026-08-06 `recognition-eval-harness` — 🧹 Завести eval-харнес распознавания: корпус кейсов и метрику точности. Причина: решено не собирать размеченный корпус руками: направление — тюнинг автоматического распознавания без участия человека. Была секция: инфраструктура.
+34
View File
@@ -0,0 +1,34 @@
# Роадмап
Что приложение уже умеет и чего ещё не умеет. Цель — файл типа `goal` в
`items/`; её задачи здесь **не перечисляются** — перечень даёт
`tasks.py list --goal <слаг>`. В `Запланировано` очередь значима и
обосновывается прозой; в `Направлениях` порядка нет; `Сопровождение` — то, чем
держат проект, а не возможности приложения; в `Готово` строку с датой пишет
`close --implemented`.
## Запланировано
Пусто. Фазы Ф0–Ф6 прежней дорожной карты (каркас, приём и трекинг,
распознавание, раскладка и ревью, метаданные, Telegram и UX, деплой) закрыты —
сквозной путь работает и развёрнут; закрытый шаг планом больше не является.
Следующая упорядоченная очередь появится, когда она понадобится.
## Направления
- [🎯 Раздача узнаётся верно без подсказок человека](items/recognition-accuracy.md) — распознавание ошибается молча и правдоподобно, а смена модели или правка промпта идёт вслепую — сдвига точности не видно ни до, ни после
- [🎯 Раскладывается не только типовая раздача](items/complex-releases.md) — типовая раздача раскладывается, а всё, что сложнее одного сезона одного тайтла, упирается в ручной разбор
- [🎯 Раздача приносится и подтверждается из любого транспорта](items/ingest-and-review-interfaces.md) — путь «принести раздачу и подтвердить догадку» упирается в незакрытые куски интерфейсов, а не в логику
- [🎯 По записи загрузки видно, как она сюда попала](items/state-integrity.md) — известные окна рассинхрона и потери маркеров: каждое по отдельности самоисцеляется, вместе — источник необъяснимых состояний
## Сопровождение
- [🎯 Сервис переживает рост и потерю тома](items/operational-resilience.md) — сервис работает, но не переживает роста: база копится вечно, бэкапа нет, отказ зависимости виден только по застрявшим задачам
- [🎯 Домен называется одинаково везде, ревью откалибровано](items/dev-process-quality.md) — наименования домена расходятся между спеками, UI и кодом, а конвейер ревью не откалиброван — растёт цена каждой следующей задачи
## Готово
Пусто. Фазы Ф0–Ф6 велись прозой и целями в роадмапе не числились, поэтому
строк с датами за ними нет; что было сделано и когда — по архиву
`openspec/changes/archive/`. Первую строку сюда впишет `close --implemented`
на первой достигнутой цели.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Пересчитывать абсолютную нумерацию аниме в SxxEyy
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** аниме со сквозной нумерацией (#137) не раскладывается в SxxEyy, который ждёт Jellyfin — нужен пересчёт абсолютной нумерации
- **Теги:** goal:complex-releases
Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а Jellyfin ждёт SxxEyy. Нужен пересчёт абсолютной нумерации в сезон/серию — надёжнее всего через TVDB (там есть absolute order). Отдельный крайний случай распознавания; на стороне ревью — веб-хелпер «absolute → S·E».
Связано: specs/recognition.md (конвейер, сезон-паки), specs/jellyfin-layout.md (нумерация серий), specs/review-ux.md.
+81
View File
@@ -0,0 +1,81 @@
# ✨ Узаконить confidence-гейт авто-раскладки в спеке и сделать его выключаемым (дефолт 0.7)
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку
- **Теги:** goal:recognition-accuracy
Аудит спек↔код (2026-07-03) нашёл расхождение: спека recognition считает
`confidence` вспомогательным сигналом (условия авто — только матч в базе +
чистая валидация + согласованность), а код (`internal/recognize/validate.go:102`)
добавляет `confidence < threshold` (дефолт 0.85) четвёртым **блокирующим**
условием.
**Решение (2026-07-08): оставляем гейт (вариант B).** Низкий confidence — это
полезная доп. проверка на ревью: LLM могла ошибиться так, что под ошибочные
данные в базе нашёлся «такой же» фильм/сериал (ложный, но самосогласованный
матч — матч и валидация чисты, а модель при этом не уверена). Такой случай ловит
именно порог, уводя задачу в review, а не в авто. Значит confidence остаётся
законным условием — но его надо честно оформить.
Что сделать:
1. **Сделать гейт реально выключаемым.** Сейчас `recognize.go:173-175`
(`if threshold <= 0 { threshold = defaultAutoThreshold }`) не даёт выключить
порог: `0` в конфиге молча возвращается к дефолту. Убрать этот фолбэк — дефолт
задаётся один раз при загрузке конфига; значение `0` → гейт пропускается
`decide` уже `p.Confidence < 0` никогда не истинно, достаточно снять
пере-применение дефолта).
2. **Понизить дефолт** `recognition.auto_confidence_threshold` 0.85 → **0.7**
(`config.go:218`, `recognize.go:144 defaultAutoThreshold`). Условия 1–3 несут
корректность; порогу остаётся ловить только по-настоящему неуверенные планы.
3. **Записать в спеку.** В `openspec/specs/recognition/spec.md` (Requirement
«Модель уверенности и решение auto/review») переформулировать: confidence —
явное **четвёртое, конфигурируемое** блокирующее условие (порог
`recognition.auto_confidence_threshold`, `0` = выкл, дефолт 0.7), а не «лишь
вспомогательный сигнал». Добавить сценарий: матч + чистая валидация +
согласованность, но `confidence` ниже порога → review.
4. **Конфиг-конвенция/дока:** описать ключ в `docs/conventions/config.md` (диапазон
[0,1], 0 = выкл, дефолт 0.7) и поправить значение в таблице «Настройки с
числовым значением» [docs/database.md](../../docs/database.md) — дом числа там, и
при смене дефолта оно разъедется первым. Формулировки инварианта в
`CLAUDE.md` и ложноположительного в `docs/review.md` уже приведены к
«единственным гейтом не является» (канон 4, 2026-08-06) — переписывать их
задача не должна.
5. **Тесты:** `decide` с `threshold=0` (гейт выключен, авто при чистых 1–3);
confidence ниже/выше порога при выполненных 1–3.
6. Точное число порога откалибровать позже по рабочему потоку — сколько раздач
ушло в ревью и сколько из них пришлось поправить. Размеченный корпус, по
которому порог можно было бы подобрать заранее, решено не собирать
(`../REJECTED.md`, 2026-08-06), так что первое значение остаётся оценкой.
## Затрагивает
- `internal/recognize``decide`/`validate.go` (четвёртое условие) и
`recognize.go` (пере-применение дефолта, из-за которого `0` не выключает
гейт);
- секция `[recognition]` конфига, `config.example.toml` и значение по умолчанию
в `internal/config`;
- `openspec/specs/recognition/spec.md` — требование «Модель уверенности и
решение auto/review»;
- `docs/database.md` (дом числа) и `docs/conventions/config.md` (описание ключа).
## Критерии приёмки
- `auto_confidence_threshold = 0` выключает гейт: при чистых условиях 1–3
раздача уходит в авто независимо от `confidence` (оракул: тест `decide` с
нулевым порогом).
- При `confidence` ниже порога и выполненных условиях 1–3 раздача уходит в
review (оракул: табличный тест на границе порога — ниже, равно, выше).
- Умолчание равно 0.7 в одном месте — загрузке конфига; `recognize.go` своего
дефолта не применяет (оракул: тест, что пустой конфиг даёт 0.7, плюс
отсутствие `defaultAutoThreshold` в диффе).
- Спека называет `confidence` четвёртым конфигурируемым блокирующим условием и
несёт сценарий «матч чист, но уверенность ниже порога → review» (оракул:
`openspec validate --strict`).
Оформить как OpenSpec-change (дельта `recognition` + правки
`validate.go`/`recognize.go`/`config`).
Связано: openspec/specs/recognition, ADR-2026-06-13-auto-link-requires-db-match,
пакет recognize.
+90
View File
@@ -0,0 +1,90 @@
# 🐞 Тормозить опрос qBittorrent бэкоффом при недоступности и эскалировать устойчивый сбой
- **Тип:** fix
- **Категория:** Инфраструктура
- **Зачем:** недоступный qBittorrent опрашивается каждые 5 с и даёт WARN на каждом тике: нужен экспоненциальный бэкофф до минутного потолка со сбросом по первому успеху и ERROR на устойчивой деградации
Остаток от задачи «классификация доменных ошибок + конвенции логирования»
(основное реализовано, см. ниже) плюс бэкофф опроса, заказанный 2026-08-06.
Речь о поведении фонового цикла, пока зависимость лежит: с какой частотой он её
дёргает и каким уровнем об этом пишет.
## Что уже сделано (не переоткрывать)
Коммит `f8fb4fa` (Tier A) + коммит этой задачи закрыли:
- **Классификация доменных ошибок:** sentinel `worker.ErrInvalidInput`→400;
обёртки `ErrConflict` в Cancel/Retry/Defer/Undo; `layout.ErrCollision`→409 в
`classifyErr` и ветка в tgbot; `logCmd` относит новые классы в DEBUG.
- **Конвенции:** `logging.md` — команды воркера = доменная граница, таблица
уровней доменных отказов (граница команды vs асинхронная стадия), правило про
`*url.Error`/секреты в URL, канон категории `state transition` (унифицированы
cancel/retry/relink/recovery). `errors.md` — таблица маппинга ошибка→статус,
развилка «транзиентный ответ vs персистентная диагностика» решена как (а):
`error_msg`/`reasons` — операторская поверхность владельца (сырой текст ок,
секреты запрещены; аудит показал, что секреты туда не текут).
- **Мелочи:** reason-коды const-блок; лог-поля `id``download_id`; preview
WARN; комментарий у `parseIgnored`.
## Остаток
**Шум `ext.*` ERROR решено оставить как есть (2026-08-06).** Запись
«зависимость недоступна» на каждом тике — легитимный сигнал транспортного слоя,
и гасится он уровнем сбора логов, а не кодом. Варианты с пониженным уровнем у
`logging.ExtCall` и с дедупом отклонены: первый заводит второе правило уровня
для того же класса вызовов, второй даёт транспортному логгеру память о
состоянии.
Остаются две вещи, и обе стоят на одном счётчике подряд-идущих сбоев тика.
**Бэкофф опроса (решение 2026-08-06).** Пока qBittorrent недоступен, цикл
продолжает дёргать его каждые `poll_interval` (5 с) — недоступную зависимость
незачем опрашивать с рабочей частотой. Интервал растёт экспоненциально от
`poll_interval` до потолка порядка минуты; первый успешный ответ возвращает
рабочий интервал сразу, без ступенчатого спуска. Бэкофф заодно снимает и остроту
шума: записей становится столько же на событие, но событий — единицы в минуту.
**Эскалация уровня.** Сейчас сбой тика — **всегда WARN**, сколько бы тиков
подряд он ни падал. `docs/conventions/logging.md` требует иного: устойчивый сбой
N тиков подряд — это реальная деградация, и она пишется ERROR.
## Воспроизведение
1. Остановить qBittorrent (локально, не на umbar).
2. Смотреть лог воркера в течение нескольких минут поллинга.
3. Наблюдается: запрос к qBittorrent уходит каждые 5 секунд всё время
недоступности, а доменная запись `poll failed` идёт WARN на каждом тике и
остаётся WARN бесконечно.
4. Ожидается: интервал опроса растёт до минутного потолка, а после N
подряд-идущих неудачных тиков уровень поднимается до ERROR — деградация
отличается от разового промаха.
5. Поднять qBittorrent обратно: опрос возвращается к `poll_interval` с первого
успешного ответа.
## Затрагивает
- цикл поллинга воркера (`internal/worker`) — счётчик подряд-идущих сбоев,
текущий интервал тика и его сброс по успеху;
- секция `[worker]` конфига и `config.example.toml` — потолок бэкоффа и порог
эскалации;
- `docs/database.md`, таблица «Настройки с числовым значением» — дом обоих
чисел;
- `docs/architecture.md`, «Характер потока» — там сказано, что фон непрерывный с
периодом поллинга; переменный интервал это уточняет;
- `docs/conventions/logging.md` — правило эскалации уже записано, меняться не
должно; задача приводит код к нему.
## Критерии приёмки
- При подряд-идущих сбоях интервал опроса растёт экспоненциально от
`poll_interval` и упирается в потолок из конфига, дальше не растёт (оракул:
тест цикла с подставным клиентом и управляемыми часами — проверяет
последовательность интервалов).
- Первый успешный ответ возвращает `poll_interval` немедленно (оракул: тот же
тест, сценарий «серия сбоев, успех, сбой» — после успеха интервал рабочий).
- Сбой тика ниже порога пишется WARN, начиная с N-го подряд — ERROR, а успешный
тик сбрасывает счётчик (оракул: тест, считающий уровни записей на сценарии
«сбой, сбой, успех, сбой»).
- Потолок бэкоффа и порог эскалации читаются из конфига и описаны в
`config.example.toml` с единицами и диапазоном (оракул: `task gate`, шаг
канона — сверка с `database.md`).
+10
View File
@@ -0,0 +1,10 @@
# 🔬 Сила совпадения кандидата и пересмотр распознавания/матчинга
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** у кандидата метабазы нет метрики силы совпадения — список кандидатов на ревью нечем отсортировать по уверенности (сперва проработать процесс матчинга)
- **Теги:** goal:recognition-accuracy
У кандидата метабазы нет метрики силы совпадения (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.
+10
View File
@@ -0,0 +1,10 @@
# 🔬 Завершение загрузки через webhook
- **Тип:** research
- **Категория:** Инфраструктура
- **Зачем:** завершение сейчас ловим поллингом qBittorrent — webhook реагировал бы быстрее, но связывает нас с его конфигом (решим по опыту эксплуатации)
- **Теги:** goal:ingest-and-review-interfaces
Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд. Альтернатива: «Run external program on torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом qBittorrent.
Связано: specs/architecture.md → «Отслеживание загрузки», пакет worker.
+12
View File
@@ -0,0 +1,12 @@
# 🎯 Раскладывается не только типовая раздача
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** типовая раздача раскладывается, а всё, что сложнее одного сезона одного тайтла, упирается в ручной разбор
- **Теги:** decomposed
Ради чего: сериальные паки, докачивание, аниме со сквозной нумерацией, образы дисков и внешние субтитры — это ровно тот контент, ради которого проект и заводился вместо arr-стека.
## Завершение
Достигнута, когда сериальный пак, докачивание недостающих серий, аниме со сквозной нумерацией, образ диска и внешние субтитры раскладываются без ручного вмешательства в файлы на диске — либо честно уходят в ревью с названной причиной, а не молча кладутся неверно.
+10
View File
@@ -0,0 +1,10 @@
# 🔬 Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** сложные раздачи (все сезоны разом, паки, смешанная нумерация) целостно не проработаны — распознавание/ревью/раскладка заточены под один сезон
- **Теги:** goal:complex-releases
Обычный случай — один сезон (его номер видно глазами и сверяем на ревью — под это сделана сводка сезонов). Но в редких заказах раздача сложнее: все сезоны сериала разом, пак нескольких сезонов, смешанная нумерация, вложенные папки сезонов, разнобойные имена файлов. Сейчас PlanFile.Season задаётся на каждом файле (мультисезон в принципе выразим), но целостно эти сценарии не проработаны: как надёжно распознать, как показать на ревью, как разложить и как стыкуется со сходимостью папки и merge-докачиванием. Решить, что поддерживаем явно, а что уводим в ревью как «сложную раскладку».
Связано: specs/recognition.md, specs/jellyfin-layout.md, specs/review-ux.md, «Проблема второго сезона», «Раздачи с докачиванием».
+59
View File
@@ -0,0 +1,59 @@
# 🔬 Кандидаты в конвенции кода
- **Тип:** research
- **Категория:** Инфраструктура
- **Зачем:** накоплен список кандидатов (внешние клиенты, конкурентность, тесты, CLI, время) — надо решить, что из них стало реальным трением, а что выдумано вперёд
- **Теги:** goal:dev-process-quality
Список копился в черновике `docs/drafts/conventions-backlog.md` (удалён при
переводе на канон, текст в истории git) под правилом «пишем по мере реального
трения, а не вперёд». Правило соблюдено — но список с тех пор не пересматривали,
а часть пунктов за это время либо реализовалась, либо механизировалась правилом
и должна из кандидатов выпасть, а не переехать в прозу.
Разобрать по одному: стало реальным трением → в `docs/conventions/`; выражается
правилом → в `.golangci.yml` или `internal/archrules` и в таблицу
«Механизировано»; выдумано вперёд → выбросить.
**Кандидаты в отдельный документ**
- **Раскладка пакетов и направление зависимостей.** `cmd/<bin>` +
`internal/<компонент>` по доменам, домен не импортирует транспорт, без свалок
`util`/`common`/`helpers`. *Частично уже механизировано* тестами
`internal/archrules` — проверить, что осталось прозой.
- **`context.Context`.** Первый параметр, не хранить в структурах, в `Value`
только request-scoped данные (не зависимости), дедлайны и отмена тянутся
сквозь стадии. Протяжка логгера уже сделана (`internal/logctx`).
- **Внешние клиенты.** Таймаут на **каждый** исходящий вызов, не
`http.DefaultClient`, ретраи с backoff и потолком, HTTP-прокси из конфига.
Кандидат на общий конструктор клиента вместо копипасты в
`qbt`/`llm`/`jellyfin`/`metadata`. Самый живой пункт: клиентов уже четыре.
- ~~**Тесты.**~~ Вынесены отдельной задачей —
[tests-convention](tests-convention.md): кандидат разошёлся с проектом
(обещал фикстуры в `testdata/`, которого нет), и разбирать его надо вместе с
тем, что накопилось в самих тестах.
**Кандидаты в строку-инвариант, а не в документ**
- **БД и миграции.** Forward-only, только параметризованные запросы, явные
транзакции для многошаговых изменений, context-aware запросы. Сильно
стек-специфично.
- **Конкурентность.** Каждая горутина знает, **как** останавливается
(ctx/закрытие канала); `errgroup` для связанных задач; фоновые процессы
гасятся при shutdown. Актуально для воркера, не для всего проекта.
- **CLI.** Данные в `stdout`, логи и диагностика в `stderr`, осмысленные коды
возврата. Для диагностических команд `add`/`recognize`/`healthcheck`.
- **Время.** Явный TZ всегда, хранение и логи в UTC. Уже частично в `CLAUDE.md`
и `conventions/logging.md`, а `time.Now` вне `store` запрещён линтером — этот
пункт, вероятно, закрыт и подлежит вычёркиванию.
- **Язык вывода связан с мапперами.** Провенанс — ревью `tvdb-title-locale`
(2026-08-07,
[отчёт триажа](../../openspec/changes/archive/2026-08-07-tvdb-title-locale/review/report.md),
находка R2 прохода `architecture`). Язык вывода живёт в пяти местах четырёх
пакетов, и теста, связывающего множество кодов языка с мапперами, нет:
забытая ветка молча даст английский вывод вместо ошибки. Кандидат в правило
`internal/archrules`, а не в прозу. **Дешёвый паллиатив, если правило окажется
дорогим:** строка «язык вывода» в `docs/architecture.md` → «Единые точки
проекта» — тогда расхождение хотя бы спросит проход по теме `architecture`.
Число мест перед правкой перепроверить: оно снято ревью одной задачи и с тех
пор могло измениться.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения
- **Тип:** feature
- **Категория:** Инфраструктура
- **Зачем:** терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации
- **Теги:** goal:operational-resilience
Терминальные задачи (done/cancelled/failed/reverted), их попытки recognition с сырыми ответами LLM и metadata_candidate копятся вечно — БД и список загрузок распухают и становятся нечитаемыми. Нужна авточистка старше N дней (настройка в [storage] или [worker]) и/или ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере эксплуатации.
Связано: specs/architecture.md → «Хранилище» (download/recognition/metadata_candidate/file_link), пакет store.
@@ -0,0 +1,10 @@
# ✨ Проверять в healthcheck доступность qBittorrent, LLM и метабаз и показывать её в UI
- **Тип:** feature
- **Категория:** Инфраструктура
- **Зачем:** /healthz проверяет только сам сервис — недоступность qBittorrent/LLM/метабазы видна лишь по застрявшим задачам, нет readiness и бейджа в UI
- **Теги:** goal:operational-resilience
/healthz проверяет только сам сервис. Если qBittorrent, LLM или метабаза недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent недоступен»), чтобы причина простоя была видна сразу.
Связано: specs/architecture.md → «Деплой» (healthcheck), пакеты qbt, llm, metadata, httpapi.
+12
View File
@@ -0,0 +1,12 @@
# 🎯 Домен называется одинаково везде, ревью откалибровано
- **Тип:** goal
- **Секция:** Сопровождение
- **Зачем:** наименования домена расходятся между спеками, UI и кодом, а конвейер ревью не откалиброван — растёт цена каждой следующей задачи
- **Теги:** decomposed
Ради чего: это не поведение продукта, а то, чем он делается. Единый словарь, калибровка проходов ревью и разбор накопленных кандидатов в конвенции — вложение в скорость всех остальных целей.
## Завершение
Достигнута, когда домен называется одинаково в спеках, коде и интерфейсе, а конвейер ревью откалиброван на журнале реальных дефектов, а не на догадках о том, что он ловит.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Раскладывать раздачу-копию диска (VIDEO_TS/BDMV) каталогом целиком
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** раздача-образ диска (VIDEO_TS/BDMV) сейчас разбирается пофайлово вместо раскладки каталога целиком — редкий, но реальный случай
- **Теги:** goal:complex-releases
Иногда для очень редких фильмов скачивается не один видеофайл, а полная копия диска — структура 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.
@@ -0,0 +1,10 @@
# ✨ Хранить историю переходов загрузки отдельной таблицей
- **Тип:** feature
- **Категория:** Инфраструктура
- **Зачем:** хранится только текущий статус загрузки — разбор «как сюда попали» идёт по логам сервера, нет таблицы истории переходов
- **Теги:** goal:state-integrity
Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто инициировал — воркер, человек, сверка), а не только текущее состояние. Сейчас по задаче виден лишь актуальный статус, разбор «как мы сюда попали» идёт по логам сервера. Отдельная таблица истории даёт лог переходов в карточке/расширенной информации и фундамент для метрик длительности стадий. Естественно ложится на собственный идентификатор загрузки и уже реализованный экран /download/{id}.
Связано: drafts/logical-title-model.md §5.4 (state_transition, actor worker|human|reconcile), specs/workflow.md, specs/database.md, пакеты worker, store.
+75
View File
@@ -0,0 +1,75 @@
# 🔬 Канон нумерации серий и порядок у провайдера тега
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега
- **Теги:** goal:complex-releases
Косметика и редкий случай: порядок просмотра не страдает (файлы уже
пронумерованы канонически и лежат по порядку), разъезжаются только подписи серий
в Jellyfin — не то название/описание у части эпизодов. Задевает лишь тайтлы с
исторически спорным порядком, таких мало.
## Проблема
У некоторых сериалов есть несколько *легитимных* порядков серий, и разные
метабазы придерживаются разных. Каноничный пример — «Ковбой Бибоп»: в титрах и
на дисках/IMDb/TVDB порядок «сессий» (Session #1 «Asteroid Blues» … #26), а TMDB
по своей политике нумерует по **самой ранней дате эфира**. Часть серий вышла
раньше на TV Tokyo вразнобой (2, 3, 7–15, 18) — при сортировке по дате они
всплывают вперёд, и диапазон ~1–18 перемешивается. Это не баг одной базы: оба
порядка «правильные», просто разные каноны. У TMDB канон отдаётся отдельной
episode group (тип DVD/production), у TVDB — отдельными order-типами
(Aired/DVD/Absolute).
## Где это бьёт по jellybit (и где нет)
jellybit **не** матчит серии по `(season, episode)` между провайдерами —
описанного класса бага у нас нет. Номер эпизода рождается из имён файлов через
LLM (`recognize.PlanFile.Episode`), проходит без изменений в раскладку и
печатается в `SxxEyy`; метабаза даёт лишь `SeasonEpisodeCounts` для гейта
полноты пака. То есть мы **доверяем нумерации релиз-группы** и про порядок вообще
не знаем.
Настоящий риск — тихий и уже существует:
> jellybit пишет `SxxEyy` **и** тег папки `[tmdbid-…]`/`[tvdbid-…]`. Дальше
> Jellyfin по этому тегу заново скрейпит серии у *того же* провайдера. Номер в
> имени файла и порядок, который ждёт скрейпер, обязаны быть **из одного
> канона** — иначе метаданные разъедутся на именно тех сериях.
Для Бибопа: релиз почти всегда пронумерован канонически (session order). Если
матч ушёл на TVDB и написан `[tvdbid-…]` — Jellyfin скрейпит aired order TVDB,
для Бибопа = канон, всё сходится. Если матч ушёл на **TMDB** и написан
`[tmdbid-…]` — Jellyfin ждёт airing order TMDB (перемешанные 1–18), а файлы
канонические → метаданные поедут. Инвариант «канон нумерации файлов ↔ провайдер
тега» сейчас нигде не проверяется.
## Что можно сделать (варианты, не решение)
- **Минимум (дёшево, ценно):** осознать инвариант и эскалировать в review, когда
у распознанного тайтла провайдер матча — из тех, где порядок известно спорный
(episode groups у TMDB, absolute order у TVDB), а нумерация файлов может не
совпадать с дефолтным скрейп-порядком этого провайдера. Лучше явный вопрос
человеку, чем тихий разъезд.
- **Предпочтение провайдера тега:** для сериалов с известным расхождением тегать
папку провайдером, чей дефолтный порядок совпадает с каноном файлов (обычно
TVDB), даже если матч найден в TMDB.
- **Максимум:** знать про порядок явно — тянуть episode group (TMDB) / order-тип
(TVDB) и сверять нумерацию файлов с выбранным каноном. Требует, чтобы у нас
появилось понятие «канон эпизода», которого сейчас в модели нет (эпизодов как
сущностей в БД нет, план — JSON-блоб).
## Связи
- Тот же класс «у тайтла несколько легитимных порядков», что и
[Аниме с абсолютной нумерацией](anime-absolute-numbering.md) (absolute
order через TVDB) — стоит проработать совместно, возможно как одну тему.
- [Сложные сериальные раздачи](complex-series-releases.md) — соседний пласт
крайних случаев раскладки.
- Схема «локальная сущность каноническая, provider id — опциональный внешний
ключ» уже заложена (draft `logical-title-model.md`, сущность `title` осознанно
отвергнута) — эту же логику надо дотянуть до эпизодов/порядка, если пойдём в
«максимум».
- specs/recognition.md (гейт полноты пака, крайние случаи), specs/jellyfin-layout.md
(нумерация серий, тег провайдера), specs/review-ux.md (эскалация в review).
+24
View File
@@ -0,0 +1,24 @@
# ✨ Спаривать VobSub .idx+.sub и проставлять языковой суффикс субтитра
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** Привязка субтитр→серия уже работает; остались пары VobSub .idx+.sub и потеря Lang/Flags
- **Теги:** goal:complex-releases
Базовая привязка субтитр→серия для сериала уже работает: `layout.PlanFile` несёт
`Season/Episode`, а `seriesDst` именует субтитр по стему эпизода
(`internal/layout/layout.go`, код старше аудита 2026-07-03). Для фильма тоже —
субтитр именуется по базе видеофайла.
Реальные пробелы, оставшиеся от аудита:
- **Пары VobSub** `.idx`+`.sub` не спариваются — их надо переносить вместе как одну
дорожку.
- **Языковой суффикс теряется:** у `recognize.PlanFile` нет полей `Lang/Flags`, и
`toLayoutPlan` (`internal/httpapi/review.go`) не проставляет язык — субтитр ляжет
без `*.ru.srt`-суффикса, который ждёт Jellyfin.
Смоделировать язык/флаги субтитра сквозь recognize→layout и спаривание `.idx`+`.sub`;
спека recognition уже требует «внешние субтитры SHALL привязываться к видео».
Связано: openspec/specs/recognition, specs/jellyfin-layout.md, пакеты recognize, layout.
@@ -0,0 +1,10 @@
# ✨ Проверять свободное место перед copy-fallback
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** copy-fallback при невозможности хардлинка может упереться в переполненный диск посреди раскладки — нет проверки места до копирования
- **Теги:** goal:operational-resilience
Когда хардлинк невозможен (EXDEV/ENOTSUP/…), layout копирует файл, дублируя место на диске. На забитом диске это упрётся в полку посреди раскладки. Перед копированием проверять доступное место и при нехватке внятно уходить в failed с понятной причиной, а не падать на полпути.
Связано: specs/architecture.md → «Раскладка файлов» (фолбэк-копирование), пакет layout.
+10
View File
@@ -0,0 +1,10 @@
# 🔬 guessit как сервис-спутник
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём
- **Теги:** goal:recognition-accuracy
go-ptn слабее питоновского guessit. Если точности пред-парса не хватит — завернуть guessit в крошечный HTTP-сервис (один файл, поставляется рядом с бинарём jellybit) и спрашивать его на шаге пред-парса. Сохраняет «доставку копированием»: два файла вместо одного.
Связано: specs/recognition.md → «На будущее» (пред-парс).
@@ -0,0 +1,14 @@
# 🐞 Связать v1/v2-хеши одного торрента и не доверять паре xt из магнета (F4, F5)
- **Тип:** fix
- **Категория:** Ядро продукта
- **Зачем:** split v1/v2 идентичность и крафт-магнет отравляют владение инфохэшами _(ревью 2026-07-08)_
- **Теги:** goal:state-integrity
Ревью 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.
@@ -0,0 +1,12 @@
# 🎯 Раздача приносится и подтверждается из любого транспорта
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** путь «принести раздачу и подтвердить догадку» упирается в незакрытые куски интерфейсов, а не в логику
- **Теги:** decomposed
Ради чего: приём и ревью — единственные места, где система встречается с человеком. Здесь копятся незакрытые куски: фетч по URL, редактор маппинга, привязка уведомлений к автору, латентность обновлений.
## Завершение
Достигнута, когда любой из поддержанных источников принимается одним действием из любого транспорта, а ревью позволяет довести план до применимого состояния без ухода в другой инструмент.
+72
View File
@@ -0,0 +1,72 @@
# 🐞 Отправлять на ревью раздачу с непомещающимся именем вместо отказа
- **Тип:** fix
- **Категория:** Ядро продукта
- **Зачем:** название длиннее ~237 байт роняет раскладку в failed с текстом системной ошибки: пользователь видит «file name too long» вместо карточки ревью, где это чинится подсказкой
Длина компонента пути ограничена файловой системой (255 байт на имя, минус
расширение и суффиксы — практический потолок около 237). Название такой длины
приходит из метабазы или из распознавания штатно: длинные оригинальные заголовки
и сериалы с подзаголовком в имя укладываются не всегда.
Сегодня это не проверяется нигде: раскладка доходит до `link(2)`, получает от ядра
`file name too long`, и задача уходит в `failed` с текстом системной ошибки в
поле ошибки. Для владельца это тупик — состояние терминальное, а причина написана
языком ядра.
Правильный исход — **review**: там человек правит название подсказкой, ровно как
при коллизии цели. Коллизия уже так и обрабатывается, то есть путь в системе
есть и его надо переиспользовать, а не изобретать.
Провенанс: ревью изменения `tvdb-title-locale`, находка AD4 —
[отчёт триажа](../../openspec/changes/archive/2026-08-07-tvdb-title-locale/review/report.md).
Понижена там не по доказательности, а по принадлежности к диффу: путь этим
изменением не тронут.
## Воспроизведение
Оракул из отчёта: подать раздачу, у которой имя целевого файла складывается в
250 байт.
```
n=250 → Apply err=… file name too long, results=0
```
Наблюдаемо: задача в состоянии `failed`, в поле ошибки — текст системной ошибки,
ни одной ссылки не создано, карточки ревью нет.
Ожидаемо: задача в `review` с названной причиной «имя не помещается», раскладка
не начата, подсказка человека чинит случай.
## Затрагивает
- `internal/layout` — проверка длины компонента до `link(2)` и доменная ошибка
вместо системной;
- `internal/worker` — ветка перевода задачи в `review` по этой ошибке, рядом с
существующей веткой коллизии цели;
- `openspec/specs/file-layout/spec.md` — требование о непомещающемся имени;
`state-reconciliation` — если переход в `review` заказывается там;
- текст причины в карточке ревью: веб-UI и Telegram показывают её человеку.
## Критерии приёмки
- Раздача с именем в 250 байт уходит в `review` с доменной причиной, а не в
`failed` с текстом ядра. **Оракул:** тест на временном каталоге прогона,
воспроизводящий случай из «Воспроизведения», — проверяет состояние задачи и
код причины.
- Ни одна ссылка не создана до отказа: частичной раскладки батча не остаётся.
**Оракул:** тот же тест — проверяет, что каталог цели пуст.
- Причина, показанная человеку, не содержит текста системной ошибки. **Оракул:**
утверждение теста на текст причины плюс конвенция
[errors.md](../../docs/conventions/errors.md) — перевод доменной ошибки на внешней
границе.
- Проверка длины стоит **до** первой операции с файловой системой. **Оракул:**
чтение диффа на ревью; тест на пустоту каталога цели его подтверждает.
## Рамки
Обрезать или переименовывать название самостоятельно нельзя — это решение
человека, у него для этого есть подсказка на ревью. Предел длины зависит от
файловой системы; зашивать 255 как универсальную константу не стоит, но и
выяснять предел у ядра в рантайме ради этой задачи не требуется — достаточно
консервативного значения с объяснением, откуда оно.
@@ -0,0 +1,17 @@
# ✨ Докладывать недостающие эпизоды merge-раскладкой при повторной заливке
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** повторная заливка сериала целиком должна доложить недостающие эпизоды merge-раскладкой, не трогая существующие ссылки — блокирует типовой сценарий свежих сериалов
- **Теги:** goal:complex-releases
Свежий сериал раздают по мере выхода: торрент с 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
+10
View File
@@ -0,0 +1,10 @@
# 🧹 Кэшировать ответы метабаз с TTL (и опционально LLM)
- **Тип:** chore
- **Категория:** Инфраструктура
- **Зачем:** повторные и ретраящиеся прогоны бьют TMDB/TVDB/TVMaze одним запросом — кэш с TTL сэкономил бы лимиты и ускорил «Распознать заново»
- **Теги:** goal:operational-resilience
Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее полезен, т.к. вход меняется подсказками).
Связано: specs/recognition.md (сверка с базой), пакеты metadata, llm.
+72
View File
@@ -0,0 +1,72 @@
# 🐞 Санитизировать название из метабазы перед подстановкой в план
- **Тип:** fix
- **Категория:** Ядро продукта
- **Зачем:** plan.Title = match.Title подставляется ПОСЛЕ sanitizePlan — название из TMDB/TVDB уезжает в имя каталога Jellyfin дословно, с невидимыми символами и гомоглифами, и авто-раскладка это пропускает
Название, пришедшее из метабазы, подставляется в план **после** того, как план
прошёл санитизацию: `plan.Title = match.Title` стоит ниже `sanitizePlan`.
Значение уезжает в имя каталога библиотеки, не сверенное ни с чем — гейт
авто-раскладки сверяет `normalize(c.Title) || normalize(c.OriginalTitle)` и
проходит по второму полю, а в путь идёт первое.
`layout.sanitizeComponent` ниже по потоку снимает `/\:*?"<>|` и байты `< 0x20`,
но категорию Cf (невидимые управляющие) не трогает и гомоглифы не сворачивает.
Итог — два визуально неотличимых каталога в медиатеке.
**Класс пре-существующий, и это важно для рамок:** тот же путь у TMDB, задача
`tvdb-title-locale` его только распространила на второго провайдера. Инвариант
«целевой путь строго под библиотекой» **держится** — проверено на `Dune/../../etc`,
`" .. "`, `"..."` и на 400 символах, выхода из песочницы нет. Поэтому `major`,
а не `critical`.
Провенанс: ревью изменения `tvdb-title-locale`, находка 2 —
[отчёт триажа](../../openspec/changes/archive/2026-08-07-tvdb-title-locale/review/report.md).
## Воспроизведение
Падающий тест на реальном `Recognizer` (оракул добыт триажом, не рассуждением).
Подать кандидата метабазы с названием и посмотреть на `plan.Title` и решение
гейта:
| Вход | Что происходит |
| --- | --- |
| три символа ZWSP (`U+200B`) | `auto=true`, в план уезжают невидимки; `sanitizeTitle` дал бы пустую строку |
| `Dune<RLO>gnp.mkv` (`U+202E`) | `auto=true`, имя каталога переворачивается при отображении |
| `Dune\nHACK` | `auto=true`, перевод строки доезжает до плана |
| `Dunа` с кириллической `а` | `auto=true`, каталог визуально неотличим от латинского `Dune` |
Во всех четырёх случаях значение в плане отличается от того, что дал бы
санитайзер, и ни одно не остановлено гейтом.
## Затрагивает
- `internal/recognize/recognize.go` — порядок подстановки `match.Title` и
`match.OriginalTitle` относительно `sanitizePlan`;
- `internal/layout` — состав `sanitizeComponent`, если решим закрывать категорию
Cf и гомоглифы здесь, а не на подстановке (выбор места — часть задачи);
- поведение **обоих** провайдеров, TMDB и TVDB: правка меняет уже работающий
TMDB, и это её главный риск;
- `openspec/specs/metadata-match/spec.md` либо `file-layout` — чьим требованием
станет «название из метабазы санитизируется перед попаданием в путь».
## Критерии приёмки
- Все четыре входа из «Воспроизведения» дают либо санитизированный `plan.Title`,
либо уход в review — но не авто-раскладку с исходным значением. **Оракул:**
тот самый падающий тест из отчёта триажа, перенесённый в дерево.
- Название, схлопывающееся санитизацией в пустую строку, не порождает каталог с
пустым именем и не роняет раскладку. **Оракул:** табличный тест на границе,
случай «три ZWSP».
- Поведение TMDB на нормальных названиях не изменилось. **Оракул:** существующие
тесты `internal/recognize` и `internal/metadata` зелёные без правок ожиданий.
- Место санитизации названо требованием спеки, а не только кодом. **Оракул:**
`openspec validate --strict` на дельте.
## Рамки
Гомоглифы сворачивать не обязательно — достаточно сделать значение в пути
предсказуемым и сверяемым; полноценная нормализация Unicode это отдельный
разговор. Инвариант «целевой путь строго под библиотекой» уже держится, ломать
его правкой нельзя. Правка задевает работающий TMDB — регресс на нём дороже
самого дефекта.
+10
View File
@@ -0,0 +1,10 @@
# 🔬 Многоступенчатая верификация привязки
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** несколько проходов распознавания с консенсусом подняли бы точность ценой стоимости/латентности — проработать, когда включать и как мерджить расхождения
- **Теги:** goal:recognition-accuracy
Несколько раз извлекать данные из раздачи и контекста разными промптами, искать в метабазах, затем сводить результаты в общий вердикт (голосование/консенсус) — выше точность ценой нескольких вызовов LLM и запросов к базам. Проработать: когда включать, как мерджить расхождения, стоимость/латентность.
Связано: specs/recognition.md (конвейер и модель уверенности).
+35
View File
@@ -0,0 +1,35 @@
# ✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена
- **Теги:** goal:complex-releases
Целевые имена для типового фильма и типового сезона заказаны
[file-layout](../../openspec/specs/file-layout/spec.md). Крайние случаи там
не заказаны: до перевода на канон они жили разделом «Крайние случаи» нарратива
`docs/specs/jellyfin-layout.md` (удалён, текст в истории git) как намерение, а
не как требование. Значит, что делает код в этих случаях, без чтения кода
неизвестно, и проверить это нечем.
Что надо определить и заказать спекой:
- **Многофайловый фильм** (фильм, разрезанный на части) — стэкинг по точному
токену Jellyfin: `Имя (Год) - part1.mkv` либо `cd1`. Точный формат уточняется
по документации Jellyfin: в нарративе он стоял с пометкой «уточнить при
реализации».
- **Редакции** — `Имя (Год) [edition-Director's Cut]` либо отдельные версии
внутри папки фильма. Смежно с задачей про репаки и версии одного тайтла, но
это про именование, а не про выбор версии.
- **Двойная серия в одном файле** — `… SxxEyy-Eyy`.
- **Спецвыпуски** — `Season 00`. Сперва проверить, не покрыты ли уже
требованием «Роли файлов на краях раздачи» в
[recognition](../../openspec/specs/recognition/spec.md).
## Критерии приёмки
<!-- 2–5 проверяемых утверждений списком, у каждого назван оракул -->
## Рамки
<!-- одна строка: чего касаться нельзя, что перезапускается, что считается необратимым -->
@@ -0,0 +1,10 @@
# ✨ Слать уведомления автору загрузки в его транспорт (мульти-бот)
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** пинги и ревью должен получать автор загрузки в своём транспорте — нет привязки загрузки к источнику/отправителю (нужно для мульти-бота)
- **Теги:** goal:ingest-and-review-interfaces
Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней. Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся единым для всех и точкой правды по функциональности (боты — тонкие адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и идентификатор отправителя, маршрутизировать пинги по нему.
Связано: specs/review-ux.md, specs/architecture.md → «Транспорты».
+12
View File
@@ -0,0 +1,12 @@
# 🎯 Сервис переживает рост и потерю тома
- **Тип:** goal
- **Секция:** Сопровождение
- **Зачем:** сервис работает, но не переживает роста: база копится вечно, бэкапа нет, отказ зависимости виден только по застрявшим задачам
- **Теги:** decomposed
Ради чего: сегодня всё держится на том, что загрузок мало и всё рядом работает. Ретеншена нет, бэкапа нет, глубокого healthcheck нет, поведение под сотней загрузок не мерялось.
## Завершение
Достигнута, когда база не растёт бесконечно, состояние переживает потерю тома, отказ любой зависимости виден владельцу раньше, чем по застрявшим задачам, и поведение под сотней одновременных загрузок измерено, а не предположено.
+67
View File
@@ -0,0 +1,67 @@
# 🧹 Откалибровать проходы ревью и завести ревьювер наименований
- **Тип:** chore
- **Категория:** Инфраструктура
- **Зачем:** конвейер ревью переехал в плагин `av-dev-pipeline`; осталась калибровка проходов на этом проекте и ревьювер наименований (ждёт словарь единого языка)
- **Теги:** goal:dev-process-quality
Набор проходов ревью поверх ревью-процесса из CLAUDE.md. Развивает ревью-процесс
OpenSpec в сторону воспроизводимых автопроверок, не заменяя человеческое ревью.
## Сделано (2026-07-10)
Заведены два кастомных ревьювера: оптика спек/требований и оптика кода
(архитектура, инварианты, конвенции, стиль, дублирование); оба подключены
чекпоинтами в пайплайн задачи.
## Сделано (2026-07-23) — переработка конвейера
Конвейер пересобран по типу проходов, а не по ролям: гейт → сверка со спекой в
обе стороны → generative-проходы → архитектура → враждебные постановки → триаж,
профили `quick`/`standard`/`deep`/`design`, контракт находок, границы покрытия,
храповик «находка → конвенция → правило → удаление», журнал проскочивших
дефектов и процедура калибровки. Подробности — ADR
[ADR-2026-07-23-review-pipeline-generative](../../docs/adr/ADR-2026-07-23-review-pipeline-generative.md).
Открытый вопрос «дробить ли проход по конвенциям на узкие оптики» закрыт:
**не дробим** — декорреляция внимания без декорреляции суждения почти не
добавляет recall, но линейно удорожает триаж.
## Сделано (2026-08-04) — переезд в плагин
Проектные копии агентов (`.claude/agents/jellybit-review-*`) и скиллов
(`review-pipeline`, `task-pipeline`, `task-batch`) удалены в пользу плагина
`av-dev-pipeline`. Проектная специфика теперь приходит из документов канона —
[docs/review.md](../../docs/review.md): типовые узлы, ложноположительные, вопросы по
темам, триггеры метки, недоступное проверке.
Два прохода плагин при этом **упразднил**, и это надо помнить:
- `idiom` — поимённая сверка со стайлгайдами языка не задаётся теперь ни одним
проходом; куда переселены способные части и почему класс признан обратимым —
[docs/review.md](../../docs/review.md) → «Перестали проверять сознательно».
- `negative` — вопрос «что опытный человек отсюда удалил бы» вошёл в
`architecture` вторым обязательным.
## Осталось
- **Ревьювер наименований** (соответствие словарю единого языка) — отдельной
оптикой не выделен: зависит от задачи «Словарь единого языка», без глоссария
проверять не по чему. Завести после неё.
- **Калибровка проходов** по процедуре `references/calibration.md` скилла
`av-dev-pipeline:review-pipeline` — ни один проход ещё не замерен инъекцией.
До замера ничего не удаляем и промпты не правим.
- **Проходы не сообщают свой потолок находок.** На прогоне `tvdb-title-locale`
(2026-08-07) о потолке промолчали четверо из шести — `autotests`, `specs`,
`architecture`, `ops`; триаж свой назвал («6 из 7, потолок не выбран»).
Устав требует объявлять сработавший потолок строкой, и без неё «находок
больше нет» неотличимо от «больше не поместилось» — то есть ровно тот сигнал,
ради которого потолок и заведён. Первый замер калибровки стоит начать с этого:
дефект прогона виден без инъекции.
- **Заполнить журнал дефектов** в [docs/review.md](../../docs/review.md) случаями,
которые уже проскочили ревью, — они станут первыми пробами калибровки.
- **Решить судьбу упразднённых проходов:** нужен ли проекту свой `idiom` поверх
плагина, или записи в «Недоступно проверке» достаточно.
Связано: CLAUDE.md (ревью-процесс, конвенции),
[docs/conventions/](../../docs/conventions/README.md), «Словарь единого языка».
+14
View File
@@ -0,0 +1,14 @@
# 🎯 Раздача узнаётся верно без подсказок человека
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** распознавание ошибается молча и правдоподобно, а смена модели или правка промпта идёт вслепую — сдвига точности не видно ни до, ни после
- **Теги:** decomposed
Ради чего: распознавание — единственное место, где система может ошибиться молча и правдоподобно. Сегодня о его точности судят по впечатлению, и сдвиг от смены модели или правки промпта заметен только задним числом.
Размеченный корпус и обучение на правках человека из этой цели исключены сознательно (`REJECTED.md`, 2026-08-06): базу руками не собираем, точность поднимаем тюнингом автоматического распознавания.
## Завершение
Достигнута, когда решение auto/review опирается на измеримую силу совпадения с записью метабазы, а не на самооценку модели, и доля раздач, ушедших в ревью или поправленных после авто-раскладки, видна по рабочему потоку и не растёт от версии к версии.
+34
View File
@@ -0,0 +1,34 @@
# ✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом
- **Теги:** goal:ingest-and-review-interfaces
Когда распознавание разложило файлы по сериям неверно, единственный путь —
подсказать текстом и перераспознать. Точечно поправить номер серии у одного
файла нельзя, а при полном провале LLM (ничего не вытащил) выхода нет вообще.
Материал, из которого задача выведена, — раздел «Объём по версиям» удалённого
нарратива `docs/specs/review-ux.md` (полный текст в истории git). Заявленный там
объём Ф5:
- **таблица «файл → серия»** с живой валидацией дыр и дублей нумерации и
кнопкой «нумеровать подряд» — частый случай, когда файлы идут по порядку, но
подписаны криво;
- **ручной режим при полном провале LLM** — выбрать тип, ввести название и год,
разложить файлы руками;
- **выбор кандидата метабазы и ввод id прямо в Telegram** — сегодня это только
в вебе, из бота идёт эскалация по deep-link.
Смежное: превью раскладки и единый список источников совпадения уже есть
([review](../../openspec/specs/review/spec.md)), так что задача про
редактирование плана, а не про его показ.
## Критерии приёмки
<!-- 2–5 проверяемых утверждений списком, у каждого назван оракул -->
## Рамки
<!-- одна строка: чего касаться нельзя, что перезапускается, что считается необратимым -->
+10
View File
@@ -0,0 +1,10 @@
# 🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000
- **Тип:** research
- **Категория:** Инфраструктура
- **Зачем:** Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
- **Теги:** goal:operational-resilience
Потолок по нагрузке нигде не зафиксирован: воркер, поллинг qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз». Записать в НФТ целевой ориентир — архитектура держит до 100 одновременных загрузок в работе (приём → распознавание → раскладка), план-максимум — 1000. Сама запись требования дешева и высокоценна: задаёт рамку для решений ниже. Отдельно (дороже) — аудит узких мест: одиночное соединение SQLite и сериализация записи, конкурентность воркера и лимит параллельных распознаваний, частота/стоимость поллинга и дедуп при наплыве.
Связано: specs/architecture.md → «Отслеживание загрузки»/«Хранилище», пакеты worker, store, qbt, llm.
+10
View File
@@ -0,0 +1,10 @@
# 🧹 Бэкапить SQLite по расписанию с ротацией
- **Тип:** chore
- **Категория:** Инфраструктура
- **Зачем:** architecture требует бекапить data-том, но стратегия не описана — сбой или редеплой стирают всё in-flight состояние (проще, пока БД маленькая)
- **Теги:** goal:operational-resilience
architecture.md требует «бекапить data-том», но как — не описано. Без понятной стратегии сбой или редеплой стирают всё in-flight состояние. Зафиксировать решение и реализовать: периодический VACUUM INTO в /data/backups по расписанию (с ротацией) либо потоковая репликация (litestream). Лучше сделать, пока БД маленькая.
Связано: specs/architecture.md → «Деплой» (data-том), пакет store.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Слать живые обновления через SSE вместо htmx-поллинга
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** живые обновления на htmx-поллинге дают задержку и холостые запросы — SSE убрал бы то и другое (поллинг работает, поэтому улучшение, не блокер)
- **Теги:** goal:ingest-and-review-interfaces
Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает, но с задержкой в интервал опроса и холостыми запросами. Перевести динамический контент (прогресс загрузки, смена статуса, раздача) на Server-Sent Events, чтобы обновления приходили почти мгновенно и без лишнего поллинга. Поллинг работает, поэтому это улучшение, а не блокер; SSE — один долгоживущий ответ на соединение, ложится на server-rendered UI без тяжёлого фронтенда.
Связано: specs/architecture.md → «Транспорты», specs/review-ux.md, пакет httpapi.
+12
View File
@@ -0,0 +1,12 @@
# 🎯 По записи загрузки видно, как она сюда попала
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** известные окна рассинхрона и потери маркеров: каждое по отдельности самоисцеляется, вместе — источник необъяснимых состояний
- **Теги:** decomposed
Ради чего: состояние загрузки — то, по чему судят обо всём остальном. Накопились известные щели: окно namer'а, идентичность split v1/v2, потеря маркера dismiss, отсутствие истории переходов.
## Завершение
Достигнута, когда по записи загрузки можно ответить «как она сюда попала», ни один известный сегодня путь не оставляет состояние, которое не объясняется историей переходов, и идентичность раздачи не подделывается входом.
+29
View File
@@ -0,0 +1,29 @@
# 🔬 Тексты и формат уведомлений в Telegram
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
- **Теги:** goal:ingest-and-review-interfaces
Зонтичная задача: пройтись по всем исходящим уведомлениям и запросам подтверждения
бота, выправить формулировки, состав данных и оформление. Тексты формируются в
`internal/tgbot/render.go`, отправка — `internal/tgbot/bot.go`; parse mode не задан
(plain text), моноширинных/жирных акцентов нет.
На что смотреть при аудите:
- **Полнота карточек:** показываем ли нужное — название, тип (фильм/сериал), год,
запись матча в метабазе, download id, причину `failed`.
- **Единый язык:** в текстах бота вперемешку «задача»/«раздача»/«загрузка» —
свести к доменному `Download` (см. [[ubiquitous-language-glossary]]).
- **Оформление:** моноширинный download id — уже сделано (HTML parse mode +
escape всех текстов, capability `notifications`); осталось при желании добавить
акценты поверх включённого parse mode.
- **Не дублировать** уже заведённое: матч метабазы в боте
[[telegram-match-metabazy]], мульти-бот адресация уведомлений
[[notification-source-binding]], выбор из нескольких находок [[telegram-vybor-nahodok]].
Итог аудита — конкретные под-задачи (эта их порождает). Проход дешёвый, при желании
приоритет можно поднять.
Связано: `internal/tgbot`, [review](../../openspec/specs/review/spec.md).
+68
View File
@@ -0,0 +1,68 @@
# 🧹 Разобрать кандидатов по тестам и записать конвенцию
- **Тип:** chore
- **Категория:** Инфраструктура
- **Зачем:** как пишем тесты, не записано нигде: пункт «Тесты» в convention-candidates не пересматривали, он обещает фикстуры в testdata/, которых в проекте нет — а трение накопилось (четыре внешних клиента, fakeStore с инъекцией ошибок, env-гейты, флаки-прогон, diff-coverage)
В `docs/conventions/` пять записей, и ни одна не про тесты. Знание о том, как
они здесь устроены, живёт в головах и в самих файлах: фикстуры чужих форматов
константами (`testdata` не заводился сознательно), интеграционные за env-гейтами
в `*_integration_test.go`, инъекция ошибок хранилища через `fakeStore`,
архитектурные правила тестом-сканером `internal/archrules` с русскими именами,
покрытие изменённых строк меряет гейт.
Пункт «Тесты» в [convention-candidates](convention-candidates.md) с тех пор не
пересматривали, и он уже расходится с проектом: обещает фикстуры в `testdata/`,
которого нет и который запрещён `CLAUDE.md`. Часть остальных кандидатов
проверяется замером, а не мнением: table-driven используется в 18 пакетах из 19,
`t.Parallel()` — ни разу, `testify` в `go.mod` не подключён.
Правило каталога конвенций — «пишем по мере реального трения, а не вперёд».
Трение здесь и есть: тема `autotests` идёт первым проходом на любой метке ревью,
а дома у неё нет — источником ей служит только семантика гейта из `CLAUDE.md`,
то есть «что краснеет», а не «что стоит проверять».
**Два кандидата пришли из ревью `tvdb-title-locale`** (2026-08-07,
[отчёт триажа](../../openspec/changes/archive/2026-08-07-tvdb-title-locale/review/report.md)
→ «Promote candidates»), оба с провенансом прохода, а не из головы:
- **один стенд чужого API на пакет.** В `tvdb_test.go` завелись два фейка одного
и того же API. Когда форма ответа поменяется по факту разведки, забытый
`fakeTVDB` останется зелёным и продолжит подтверждать опровергнутое
предположение;
- **интеграционный тест обязан утверждать, а не печатать.** `TestIntegration_TVDB`
печатает результат и ничего не проверяет: `PASS` с пустым выводом неотличим от
подтверждения. Тест, который нельзя провалить, оракулом не является.
## Затрагивает
- `docs/conventions/tests.md` — новый файл записи конвенции;
- `docs/conventions/README.md` — строка в индексе «Записи» и, возможно, строки в
таблице «Механизировано»;
- `tasks/items/convention-candidates.md` — пункт «Тесты» уходит из списка
кандидатов;
- `.golangci.yml` и `internal/archrules` — только если что-то из решённого
выражается правилом, а не прозой.
## Критерии приёмки
- Каждый кандидат пункта «Тесты» получил исход поимённо: в прозу, в правило или
выброшен как выдуманный вперёд. **Оракул:** в `convention-candidates.md`
пункта «Тесты» больше нет, а судьба каждого его подпункта названа — в новой
конвенции, в таблице «Механизировано» или в `REJECTED`-строке причины.
- `docs/conventions/tests.md` заведён и назван в индексе. **Оракул:** `task
gate`, шаг `canon` — файл вне канона и битая ссылка краснят безусловно.
- Ни одно утверждение конвенции не расходится с тем, как код устроен сегодня:
фикстуры описаны как лежат, `testdata` не обещан, число пакетов и приёмов не
выдумано. **Оракул:** `grep -r testdata` по репозиторию пуст, и агент
`doc-code-drift` на ближайшей сессии не даёт находки по этому файлу.
- Выражаемое правилом не осталось прозой. **Оракул:** каждое утверждение новой
конвенции либо отсутствует в таблице «Механизировано», либо стоит там с
адресом механизации — конфигом линтера или тестом `internal/archrules`.
## Рамки
Существующие тесты под новую конвенцию не переписываются — она применяется к
тому, что пишется дальше. `testdata/` не заводится: отказ от него записан в
`CLAUDE.md`, и его пересмотр — отдельное решение, а не побочный эффект этой
задачи.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Заменять разложенную версию тайтла новой (репаки, апгрейд 1080p → 2160p)
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** По калибровке болей (2026-07-02) — не боль, из приоритета выпало
- **Теги:** goal:complex-releases
По калибровке болей (2026-07-02) — не боль, из приоритета выпало. Сосуществование версий доступно уже сейчас (Jellyfin multi-version, другой целевой путь), коллизия на тот же путь штатно уходит в review. Явный replace (undo старого хардлинка → lay нового → супересид владения путём) — отдельный change, если/когда станет болью.
Связано: «Раздачи с докачиванием», specs/jellyfin-layout.md (never-overwrite, коллизия).
+22
View File
@@ -0,0 +1,22 @@
# ✨ Принимать ссылку на .torrent и скачивать файл самим (нужен SSRF-гард)
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** magnet и .torrent-файл приняты; остался фетч .torrent по URL (нужен SSRF-гард)
- **Теги:** goal:ingest-and-review-interfaces
Приём magnet и `.torrent`-файла уже реализован: ветка `TorrentData → torrent.Parse`
(`internal/ingest/ingest.go`), файл-пикер в веб-форме (`web/templates/index.html`),
приём документа в Telegram (`internal/tgbot`). Осталась одна ветка «единого окна» —
**URL на `.torrent`**: сервис сам скачивает файл по ссылке и заводит загрузку. Была
осознанно вынесена из change `torrent-file-ingest`, потому что требует исходящего
запроса на пользовательский URL → нужен SSRF-гард (allowlist схем/хостов, запрет
приватных сетей, лимит размера/редиректов).
Идеал по-прежнему — одно поле, куда кидают текст или файл, а сервис разбирает, что
это (magnet / ссылка на .torrent / .torrent-файл / сообщение бота). Сейчас текстовое
поле идёт только через `magnet.Parse`.
Связано: [architecture.md](../../docs/architecture.md) → «Внешние границы и форматы»
(`source_type = magnet|torrent|url` уже в схеме), пакет `ingest`, архив change
`torrent-file-ingest`.
@@ -0,0 +1,72 @@
# 🔬 Форма ответа поиска TheTVDB и семантика параметра language
- **Тип:** research
- **Категория:** Ядро продукта
- **Зачем:** форма ответа поиска TheTVDB принята по swagger 4.7.10 и живым прогоном не подтверждена — при иной форме разбор молча уходит в фолбэк, гейт зелёный, локализованное название не работает
- **Теги:** goal:recognition-accuracy
Задача `tvdb-title-locale` научила клиент TVDB брать локализованное название из
блока переводов ответа `/search` и заполнять `OriginalTitle` primary name'ом. Но
живым прогоном форма ответа не сверялась: `CLAUDE.md` → «Запреты» запрещает
ходить в боевые метабазы из отладочных прогонов и расходовать лимиты ключа.
Форма взята из публичной документации (swagger TheTVDB v4, версия `4.7.10`) и
записана в [docs/research/tvdb-search-translations.md](../../docs/research/tvdb-search-translations.md)
как **условие, а не замер**.
Разведка нужна потому, что ошибка предположения **не наблюдаема**: разбор уйдёт в
тотальный фолбэк, `Title` станет равен primary name, то есть исход побайтно
совпадёт с поведением до задачи. Гейт зелёный, карточка ревью прежняя, фича не
работает. Единственный след — DEBUG-строка «в выдаче не разобрался ни один блок
переводов».
## Вопрос
Какова реальная форма блока переводов в ответе `/search` TheTVDB v4 — карта
«трёхбуквенный код языка → название» или иная, — и правда ли параметр `language`
этого эндпоинта сужает выдачу по основному языку записи, а не выбирает перевод?
## Куда ляжет ответ
- [docs/research/tvdb-search-translations.md](../../docs/research/tvdb-search-translations.md):
предположения заменяются наблюдениями с датой прогона, а строка «живым
прогоном не подтверждено» — результатом. Условие пересмотра там уже записано.
- Решение по трём развилкам, оставшимся от `tvdb-title-locale`:
1. **Критерий приёмки про параметр языка в запросе.** Задача требовала, чтобы
запрос поиска содержал параметр языка из `[general].language`. При
реализации критерий отменён: по документации параметр — фильтр («Restrict
results to a specific primary language»), и его передача отсекла бы ровно
иноязычные записи, ради которых задача заводилась. Тест сейчас проверяет
обратное — что параметра нет. Критерий менял исполнитель, а не приёмщик;
нужно решение человека: переписать критерий под факт, отвергнуть решение
или подтвердить семантику прогоном и решить по факту. Рекомендация —
подтвердить прогоном, затем переписать критерий.
2. **Форма блока `translations`.** Подтвердить карту и трёхбуквенность ключей
либо починить разбор под реальную форму.
3. **Граница авто-раскладки сдвинулась в обе стороны.** Заполнение
`OriginalTitle` даёт кандидату TVDB два названия вместо одного. Логика
гейта не менялась, вход изменился: запись, которую отсекал иероглифический
primary name, теперь может пройти по переводу (review → авто), а две разные
записи могут совпасть с планом разными названиями (авто → review —
франшиза с одним русским названием и годами в пределах ±1). Рекомендация —
принять как есть: движение вниз ведёт к человеку, движение вверх и есть
польза задачи, и это ровно то, как уже работает TMDB. Альтернатива —
сравнивать у TVDB только по `OriginalTitle`, но тогда пропадает польза от
совпадения по переводу.
## Рамки
Оракул уже написан и лежит в репозитории — прогоняется вручную, человеком, под
своим ключом:
```
TVDB_API_KEY=… go test ./internal/metadata/ -run Integration -v
```
Он печатает `Title` и `OriginalTitle` для `Fargo` и для иноязычной записи
`Ne Zha` (movie 131155, primary name `哪吒之魔童降世`, русский перевод «Нэчжа»).
Расхождение `Title` и `OriginalTitle` у второй записи подтверждает форму;
совпадение означает, что перевод не доехал.
Автоматическим прогоном разведка не делается: лимиты ключа беречь, в гейт этот
тест не заводить. Кода менять не требуется — исход разведки это запись; правка
разбора, если форма окажется иной, заводится отдельной задачей.
@@ -0,0 +1,10 @@
# 🧹 Свести термины домена в словарь единого языка
- **Тип:** chore
- **Категория:** Инфраструктура
- **Зачем:** наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
- **Теги:** goal:dev-process-quality
Свести термины домена в один глоссарий, чтобы пользователь, документация, код и агент говорили на одном языке: загрузка, раздача, распознавание, матч, кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и т.д. — русский термин, английский идентификатор в коде, краткое определение. Сейчас наименования расходятся между спеками, UI и кодом. Глоссарий — источник истины по именам; на нём же строится агент-ревьювер наименований.
Связано: docs/conventions, specs/architecture.md, новый файл-глоссарий.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Закрыть веб-UI авторизацией, когда доверенной LAN станет мало
- **Тип:** feature
- **Категория:** Инфраструктура
- **Зачем:** для v1 решено без авторизации (доверенная LAN, опц. allowlist подсетей) — задел на случай, если понадобится защита
- **Теги:** goal:ingest-and-review-interfaces
Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей (http.trusted_subnets) — как умеет qBittorrent. Если понадобится защита: токен/Basic в самом приложении или вынос за reverse-proxy с аутентификацией.
Связано: specs/architecture.md → «Транспорты» (доступ к веб-UI), пакет httpapi.
+10
View File
@@ -0,0 +1,10 @@
# ✨ Переделать веб-UI в устанавливаемое PWA
- **Тип:** feature
- **Категория:** Ядро продукта
- **Зачем:** текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
- **Теги:** goal:ingest-and-review-interfaces
Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое, удобное с телефона). Текущий server-rendered UI функционален, поэтому это улучшение, а не блокер; большой объём работы.
Связано: specs/review-ux.md (веб = точные правки), пакет httpapi.