docs: перевод документации на канон av-dev
- Раскладка docs/ приведена к канону 2: заведены passport/architecture/ database/security/review и research; docs/specs, drafts, backlog, review/ и BRIEF.md разобраны и удалены, беклог переехал в docs/tasks (34 задачи, 6 целей, слаги на английский). - Нарративы specs удалены как дубли openspec-спек после поимённой сверки; остаток заведён задачами (редактор маппинга ревью, крайние случаи именования), отказ от сущности title промоутнут в ADR. - Проектные копии агентов и скиллов ревью/пайплайна удалены в пользу плагинов av-dev-pm и av-dev-pipeline; в task gate добавлен шаг canon вместо er-schema.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Аниме с абсолютной нумерацией
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** аниме со сквозной нумерацией (#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.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Confidence-гейт авто-раскладки: узаконить в спеке + сделать выключаемым (дефолт 0.7)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** Решено (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).
|
||||
5. **Тесты:** `decide` с `threshold=0` (гейт выключен, авто при чистых 1–3);
|
||||
confidence ниже/выше порога при выполненных 1–3.
|
||||
6. Точное число порога откалибровать позже — для этого есть задача
|
||||
[eval-харнес распознавания](recognition-eval-harness.md) (сейчас гейтим по
|
||||
неизмеренному сигналу).
|
||||
|
||||
Оформить как OpenSpec-change (дельта `recognition` + правки
|
||||
`validate.go`/`recognize.go`/`config`).
|
||||
|
||||
Связано: openspec/specs/recognition, ADR-2026-06-13-auto-link-requires-db-match,
|
||||
[eval-харнес](recognition-eval-harness.md), пакет recognize.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Шум ERROR фоновых циклов при недоступной зависимости
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** Остаток задачи логирования: ext.* ERROR-шторм при недоступном qBittorrent + эскалация устойчивого сбоя тика _(ревью Fable)_
|
||||
- **Теги:** goal:operational-resilience
|
||||
|
||||
Остаток от задачи «классификация доменных ошибок + конвенции логирования»
|
||||
(основное реализовано, см. ниже). Здесь — два смежных пункта про уровень
|
||||
повторяющихся сбоев фоновых циклов, каждый требует небольшого решения, а не
|
||||
только правки.
|
||||
|
||||
## Что уже сделано (не переоткрывать)
|
||||
|
||||
Коммит `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`.
|
||||
|
||||
## Остаток
|
||||
|
||||
### ERROR-шторм при недоступном qBittorrent
|
||||
|
||||
Клиент `qbt` логирует `ext.*` `Failure` → **ERROR** на каждом тике поллинга
|
||||
(`torrents/info`, `internal/qbt/qbt.go`), пока qBittorrent недоступен (рестарт
|
||||
демона, сеть). Домен уже пишет `poll failed` = WARN (по новой конвенции), но
|
||||
транспортная `ext.*`-запись остаётся ERROR по правилу ext-конвенции («сервис
|
||||
недоступен → ERROR»). При частом поллинге это шумит.
|
||||
|
||||
Развилка (решить до правки):
|
||||
|
||||
- (а) Ввести у `logging.ExtCall` вариант с пониженным уровнем для рутинно-частых
|
||||
вызовов (симметрично `SuccessDebug`) — поллинг-вызовы (`torrents/info`) на
|
||||
транзиентном сбое пишут WARN, не ERROR;
|
||||
- (б) Дедуп/circuit-breaker: первый ERROR, дальше тишина до восстановления;
|
||||
- (в) Оставить как есть, признав `ext.*` ERROR легитимным сигналом «зависимость
|
||||
лежит» (тогда шум гасить уровнем сбора, а не кодом).
|
||||
|
||||
### Эскалация устойчивого сбоя тика
|
||||
|
||||
Сейчас транзиентный сбой тика = WARN всегда. Договорённость на будущее
|
||||
(`logging.md`): устойчивый сбой N тиков подряд эскалировать в ERROR (реальная
|
||||
деградация, а не разовый промах). Не реализовано — нужен счётчик подряд-сбоев по
|
||||
циклу и порог в конфиге.
|
||||
|
||||
Вердикт: мелкая надёжностная полировка, не блокер. Делать вместе (обе про
|
||||
уровень сбоев фоновых циклов) или отдельной строкой.
|
||||
@@ -0,0 +1,9 @@
|
||||
# [idea] Сила совпадения кандидата и пересмотр распознавания/матчинга
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** у кандидата метабазы нет метрики силы совпадения — список кандидатов на ревью нечем отсортировать по уверенности (сперва проработать процесс матчинга)
|
||||
- **Теги:** 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.
|
||||
@@ -0,0 +1,60 @@
|
||||
# `addReq` не пересобирается из свежего `source_type` перед `Add` (окно namer'а)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** При апгрейде magnet→.torrent в окне namer'а добавится magnet из устаревшего снимка; самоисцеляется через magnet_timeout→failed→Retry _(аудит 2026-07-17)_
|
||||
- **Теги:** goal:state-integrity
|
||||
|
||||
Найдено аудитом capability **download-tracking** (сверка код↔спека после пачки
|
||||
lifecycle-задач). Пред-существующее, вне scope задачи F3/cancel-cleanup — T4
|
||||
осознанно вынес это за рамки и задокументировал в своём design.md.
|
||||
|
||||
## Суть
|
||||
|
||||
`processCatched` (`internal/worker/worker.go:442-517`) строит `addReq` из записи
|
||||
`cur`, перечитанной под замком на `:447-455`, **до** вызова namer'а (LLM, секунды,
|
||||
вне замка, `:463-478`). Затем на `:484-491` под замком перечитывается `before`,
|
||||
но `addReq` из него **не пересобирается** — проверяется только `state == catched`.
|
||||
|
||||
Если апгрейд пойманной magnet-задачи до `.torrent`
|
||||
(`UpgradeCatchedMagnetToTorrent`, `internal/store/download.go:392`) отработает
|
||||
именно в окне namer'а (приём принял `.torrent` с тем же infohash, `state`
|
||||
остаётся `catched`), воркер добавит **magnet-ссылку из устаревшего снимка**, хотя
|
||||
в БД уже `source_type=torrent`.
|
||||
|
||||
Спека (`openspec/specs/download-tracking/spec.md`, раздел про добавление по
|
||||
`source_type`) требует перечитывать `source_type` **под блокировкой переходов
|
||||
непосредственно перед добавлением** — сейчас это требование в окне namer'а
|
||||
нарушается.
|
||||
|
||||
## Насколько больно
|
||||
|
||||
Ограниченно и самоисцеляемо: на закрытом трекере magnet без метаданных зависнет
|
||||
в `metaDL` → предохранитель `magnet_timeout` → `failed`; ручной `Retry`
|
||||
перечитает актуальный `source_type` и добьёт. Данные не страдают, инвариант
|
||||
«источник неприкосновенен» не задет. Окно узкое (апгрейд должен лечь ровно в
|
||||
LLM-вызов по тому же infohash). Поэтому средний, не высокий.
|
||||
|
||||
## Развилка (решить до кода)
|
||||
|
||||
- **A — ужесточить код (соответствие букве спеки, закрыть окно):** после re-read
|
||||
`before` под замком (`:484`) пересобирать `addReq`/`hint` из `before`, если
|
||||
`source_type` изменился. Нюанс: `sourceAddParts` читает байты `.torrent` — это
|
||||
тяжёлый вызов, держать под замком нельзя (спека: тяжёлое — вне блокировки), плюс
|
||||
подсказка имени для `.torrent` иная (метаданные раздачи vs имя из magnet), т.е.
|
||||
при апгрейде корректно был бы и повторный namer. Не однострочник.
|
||||
- **B — смягчить спеку (принять реальность):** признать, что рациональ («не
|
||||
полагаться на снимок, снятый ранее вне блокировки») уже выполнен первым re-read
|
||||
под замком на `:447`, и переформулировать требование как «перечитывать
|
||||
`source_type` под блокировкой после тик-снимка», явно приняв узкое namer-окно
|
||||
как самоисцеляемое через `Retry`.
|
||||
|
||||
Рекомендация — начать с B (дёшево, отражает фактическое осознанное поведение), A
|
||||
завести только если узкое окно окажется реальной болью в эксплуатации.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- `internal/worker/worker.go:442-517` — `processCatched`
|
||||
- `internal/store/download.go:392` — `UpgradeCatchedMagnetToTorrent`
|
||||
- `openspec/specs/download-tracking/spec.md` — требование про `source_type`
|
||||
- Тест `TestProcessCatchedReReadsSourceTypeUnderLock` покрывает апгрейд между
|
||||
тик-снимком и re-read, но **не** окно namer'а.
|
||||
@@ -0,0 +1,9 @@
|
||||
# [idea] Завершение загрузки через webhook
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** завершение сейчас ловим поллингом qBittorrent — webhook реагировал бы быстрее, но связывает нас с его конфигом (решим по опыту эксплуатации)
|
||||
- **Теги:** goal:ingest-and-review-interfaces
|
||||
|
||||
Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд. Альтернатива: «Run external program on torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом qBittorrent.
|
||||
|
||||
Связано: specs/architecture.md → «Отслеживание загрузки», пакет worker.
|
||||
@@ -0,0 +1,11 @@
|
||||
# [goal] Сложные раздачи
|
||||
|
||||
- **Секция:** темы
|
||||
- **Зачем:** типовая раздача раскладывается, а всё, что сложнее одного сезона одного тайтла, упирается в ручной разбор
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: сериальные паки, докачивание, аниме со сквозной нумерацией, образы дисков и внешние субтитры — это ровно тот контент, ради которого проект и заводился вместо arr-стека.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда сериальный пак, докачивание недостающих серий, аниме со сквозной нумерацией, образ диска и внешние субтитры раскладываются без ручного вмешательства в файлы на диске — либо честно уходят в ревью с названной причиной, а не молча кладутся неверно.
|
||||
@@ -0,0 +1,9 @@
|
||||
# [idea] Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** сложные раздачи (все сезоны разом, паки, смешанная нумерация) целостно не проработаны — распознавание/ревью/раскладка заточены под один сезон
|
||||
- **Теги:** goal:complex-releases
|
||||
|
||||
Обычный случай — один сезон (его номер видно глазами и сверяем на ревью — под это сделана сводка сезонов). Но в редких заказах раздача сложнее: все сезоны сериала разом, пак нескольких сезонов, смешанная нумерация, вложенные папки сезонов, разнобойные имена файлов. Сейчас PlanFile.Season задаётся на каждом файле (мультисезон в принципе выразим), но целостно эти сценарии не проработаны: как надёжно распознать, как показать на ревью, как разложить и как стыкуется со сходимостью папки и merge-докачиванием. Решить, что поддерживаем явно, а что уводим в ревью как «сложную раскладку».
|
||||
|
||||
Связано: specs/recognition.md, specs/jellyfin-layout.md, specs/review-ux.md, «Проблема второго сезона», «Раздачи с докачиванием».
|
||||
@@ -0,0 +1,47 @@
|
||||
# [idea] Кандидаты в конвенции кода
|
||||
|
||||
- **Секция:** Инфраструктура
|
||||
- **Зачем:** накоплен список кандидатов (внешние клиенты, конкурентность, тесты, 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`. Самый живой пункт: клиентов уже четыре.
|
||||
- **Тесты.** Table-driven, фикстуры в `testdata/`, `t.Parallel()` где
|
||||
безопасно, зафиксировать stdlib `testing` против `testify`, разделение
|
||||
быстрых и интеграционных (`*_integration_test.go` + env-гейты уже есть), что
|
||||
считаем обязательным к покрытию.
|
||||
|
||||
**Кандидаты в строку-инвариант, а не в документ**
|
||||
|
||||
- **БД и миграции.** Forward-only, только параметризованные запросы, явные
|
||||
транзакции для многошаговых изменений, context-aware запросы. Сильно
|
||||
стек-специфично.
|
||||
- **Конкурентность.** Каждая горутина знает, **как** останавливается
|
||||
(ctx/закрытие канала); `errgroup` для связанных задач; фоновые процессы
|
||||
гасятся при shutdown. Актуально для воркера, не для всего проекта.
|
||||
- **CLI.** Данные в `stdout`, логи и диагностика в `stderr`, осмысленные коды
|
||||
возврата. Для диагностических команд `add`/`recognize`/`healthcheck`.
|
||||
- **Время.** Явный TZ всегда, хранение и логи в UTC. Уже частично в `CLAUDE.md`
|
||||
и `conventions/logging.md`, а `time.Now` вне `store` запрещён линтером — этот
|
||||
пункт, вероятно, закрыт и подлежит вычёркиванию.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Ретеншн и очистка БД
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** терминальные задачи и сырые ответы 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,9 @@
|
||||
# Глубокий healthcheck и статус зависимостей
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** /healthz проверяет только сам сервис — недоступность qBittorrent/LLM/метабазы видна лишь по застрявшим задачам, нет readiness и бейджа в UI
|
||||
- **Теги:** goal:operational-resilience
|
||||
|
||||
/healthz проверяет только сам сервис. Если qBittorrent, LLM или метабаза недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent недоступен»), чтобы причина простоя была видна сразу.
|
||||
|
||||
Связано: specs/architecture.md → «Деплой» (healthcheck), пакеты qbt, llm, metadata, httpapi.
|
||||
@@ -0,0 +1,11 @@
|
||||
# [goal] Процесс и качество разработки
|
||||
|
||||
- **Секция:** темы
|
||||
- **Зачем:** наименования домена расходятся между спеками, UI и кодом, а конвейер ревью не откалиброван — растёт цена каждой следующей задачи
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: это не поведение продукта, а то, чем он делается. Единый словарь, калибровка проходов ревью и разбор накопленных кандидатов в конвенции — вложение в скорость всех остальных целей.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда домен называется одинаково в спеках, коде и интерфейсе, а конвейер ревью откалиброван на журнале реальных дефектов, а не на догадках о том, что он ловит.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Раздачи-копии диска (DVD/BluRay: VIDEO_TS/BDMV)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** раздача-образ диска (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,49 @@
|
||||
# Веб-UI зовёт Cancel вместо Dismiss на не-терминальных → теряется `user_dismiss`
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** Функционально ок (Cancel даёт cancelled), но маркер user_dismiss в error_code теряется; расхождение с буквой спеки _(аудит 2026-07-17)_
|
||||
- **Теги:** goal:state-integrity
|
||||
|
||||
Найдено аудитом capability **state-reconciliation** (сверка код↔спека).
|
||||
Пред-существующее, вне scope пачки lifecycle-задач.
|
||||
|
||||
## Суть
|
||||
|
||||
Спека `openspec/specs/state-reconciliation/spec.md` (требование «Ручное
|
||||
закрытие»): команда `dismiss` доступна из **любого** состояния кроме `deleted`
|
||||
**во всех транспортах**, и переход SHALL помечаться `error_code = user_dismiss`.
|
||||
|
||||
В веб-UI danger-zone «Закрыть» (dismiss) гейтится только для терминальных
|
||||
состояний: `Dismissable = IsTerminal() && !deleted && !cancelled`
|
||||
(`internal/httpapi/download.go:130`, шаблон
|
||||
`web/templates/partials/download_main.html:99-112`). Для НЕ-терминальных
|
||||
(`stuck`, `deferred`, `downloading`, `review`) закрытие в UI идёт кнопкой
|
||||
«Отменить» → `Cancel` (`internal/worker/worker.go:973`), которая пишет **пустой**
|
||||
`error_code`, а не `user_dismiss`.
|
||||
|
||||
## Насколько больно
|
||||
|
||||
Функционально сценарии проходят: `Cancel` тоже даёт `cancelled` и не трогает
|
||||
файлы/раздачу, семантика для пользователя идентична. Состояния без доступного
|
||||
«закрытия» нет (кроме `deleted`/`cancelled`). Теряется только маркер
|
||||
`user_dismiss` в `error_code` — расхождение с буквой спеки и небольшая потеря
|
||||
наблюдаемости (в аналитике/логах не отличить «пользователь закрыл активную» от
|
||||
«пользователь отменил»). Отсюда низкий приоритет.
|
||||
|
||||
## Развилка (решить до кода)
|
||||
|
||||
- **A — привести код к спеке:** веб-UI на не-терминальных тоже зовёт `Dismiss`
|
||||
ради единого маркера `user_dismiss`; либо `Cancel` пишет `user_dismiss`.
|
||||
- **B — привести спеку к коду:** зафиксировать осознанное разделение (`Cancel`
|
||||
для активных, `Dismiss` для терминальных) — уточнить требование, что стоп-кран
|
||||
на не-терминальных реализуется `Cancel`'ом, и определить, какой `error_code`
|
||||
ожидается.
|
||||
|
||||
Сначала решить, осознанно ли разделение Cancel/Dismiss; если да — вероятно B.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- `internal/httpapi/download.go:130` — гейт `Dismissable`
|
||||
- `web/templates/partials/download_main.html:99-112` — danger-zone
|
||||
- `internal/worker/worker.go:973` — `Cancel`; `:1001` — `Dismiss`
|
||||
- `openspec/specs/state-reconciliation/spec.md` — требование «Ручное закрытие»
|
||||
@@ -0,0 +1,9 @@
|
||||
# История переходов загрузки
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** хранится только текущий статус загрузки — разбор «как сюда попали» идёт по логам сервера, нет таблицы истории переходов
|
||||
- **Теги:** 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.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Согласование канона нумерации серий с провайдером тега
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) 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).
|
||||
@@ -0,0 +1,23 @@
|
||||
# Внешние субтитры: пары VobSub и языковой суффикс
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** Привязка субтитр→серия уже работает; остались пары 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,9 @@
|
||||
# Проверка свободного места перед copy-fallback
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** copy-fallback при невозможности хардлинка может упереться в переполненный диск посреди раскладки — нет проверки места до копирования
|
||||
- **Теги:** goal:operational-resilience
|
||||
|
||||
Когда хардлинк невозможен (EXDEV/ENOTSUP/…), layout копирует файл, дублируя место на диске. На забитом диске это упрётся в полку посреди раскладки. Перед копированием проверять доступное место и при нехватке внятно уходить в failed с понятной причиной, а не падать на полпути.
|
||||
|
||||
Связано: specs/architecture.md → «Раскладка файлов» (фолбэк-копирование), пакет layout.
|
||||
@@ -0,0 +1,9 @@
|
||||
# [idea] guessit как сервис-спутник
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём
|
||||
- **Теги:** goal:recognition-accuracy
|
||||
|
||||
go-ptn слабее питоновского guessit. Если точности пред-парса не хватит — завернуть guessit в крошечный HTTP-сервис (один файл, поставляется рядом с бинарём jellybit) и спрашивать его на шаге пред-парса. Сохраняет «доставку копированием»: два файла вместо одного.
|
||||
|
||||
Связано: specs/recognition.md → «На будущее» (пред-парс).
|
||||
@@ -0,0 +1,13 @@
|
||||
# Идентичность инфохэшей: split v1/v2 одного торрента + крафт-магнет отравляет владение (F4, F5)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** 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,11 @@
|
||||
# [goal] Интерфейсы приёма и ревью
|
||||
|
||||
- **Секция:** темы
|
||||
- **Зачем:** путь «принести раздачу и подтвердить догадку» упирается в незакрытые куски интерфейсов, а не в логику
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: приём и ревью — единственные места, где система встречается с человеком. Здесь копятся незакрытые куски: фетч по URL, редактор маппинга, привязка уведомлений к автору, латентность обновлений.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда любой из поддержанных источников принимается одним действием из любого транспорта, а ревью позволяет довести план до применимого состояния без ухода в другой инструмент.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Нити приёма: NoName в контексте, устаревшие комментарии, лог без причины, bencode-аллокации (N1, N3, N4, N5)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_
|
||||
- **Теги:** goal:state-integrity
|
||||
|
||||
Ревью 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-путей там не найдено.)
|
||||
|
||||
Вердикт: простые фиксы/принять.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Обучение на правках человека (few-shot из прошлых ревью)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** правки человека (матч/тип/нумерация) не переиспользуются — few-shot из прошлых ревью поднял бы точность на «своих» трекерах без смены модели
|
||||
- **Теги:** goal:recognition-accuracy
|
||||
|
||||
Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать похожие в будущие промпты. Системно повышает точность на «твоих» трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой верификации, но дешевле: учимся на уже собранных hint/override.
|
||||
|
||||
Связано: specs/recognition.md (конвейер, промпт), «Многоступенчатая верификация», specs/architecture.md → «Хранилище» (hint, override).
|
||||
@@ -0,0 +1,16 @@
|
||||
# Раздачи с докачиванием (merge при повторном добавлении)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** повторная заливка сериала целиком должна доложить недостающие эпизоды 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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Кэш метабаз (и опционально LLM)
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** повторные и ретраящиеся прогоны бьют TMDB/TVDB/TVMaze одним запросом — кэш с TTL сэкономил бы лимиты и ускорил «Распознать заново»
|
||||
- **Теги:** goal:operational-resilience
|
||||
|
||||
Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее полезен, т.к. вход меняется подсказками).
|
||||
|
||||
Связано: specs/recognition.md (сверка с базой), пакеты metadata, llm.
|
||||
@@ -0,0 +1,9 @@
|
||||
# [idea] Многоступенчатая верификация привязки
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** несколько проходов распознавания с консенсусом подняли бы точность ценой стоимости/латентности — проработать, когда включать и как мерджить расхождения
|
||||
- **Теги:** goal:recognition-accuracy
|
||||
|
||||
Несколько раз извлекать данные из раздачи и контекста разными промптами, искать в метабазах, затем сводить результаты в общий вердикт (голосование/консенсус) — выше точность ценой нескольких вызовов LLM и запросов к базам. Проработать: когда включать, как мерджить расхождения, стоимость/латентность.
|
||||
|
||||
Связано: specs/recognition.md (конвейер и модель уверенности).
|
||||
@@ -0,0 +1,34 @@
|
||||
# Крайние случаи именования: многофайловый фильм, редакции, двойная серия
|
||||
|
||||
- **Секция:** Ядро продукта
|
||||
- **Зачем:** стэкинг частей (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,9 @@
|
||||
# Привязка уведомлений к источнику в ботах (мульти-бот)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** пинги и ревью должен получать автор загрузки в своём транспорте — нет привязки загрузки к источнику/отправителю (нужно для мульти-бота)
|
||||
- **Теги:** goal:ingest-and-review-interfaces
|
||||
|
||||
Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней. Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся единым для всех и точкой правды по функциональности (боты — тонкие адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и идентификатор отправителя, маршрутизировать пинги по нему.
|
||||
|
||||
Связано: specs/review-ux.md, specs/architecture.md → «Транспорты».
|
||||
@@ -0,0 +1,11 @@
|
||||
# [goal] Эксплуатационная прочность
|
||||
|
||||
- **Секция:** темы
|
||||
- **Зачем:** сервис работает, но не переживает роста: база копится вечно, бэкапа нет, отказ зависимости виден только по застрявшим задачам
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: сегодня всё держится на том, что загрузок мало и всё рядом работает. Ретеншена нет, бэкапа нет, глубокого healthcheck нет, поведение под сотней загрузок не мерялось.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда база не растёт бесконечно, состояние переживает потерю тома, отказ любой зависимости виден владельцу раньше, чем по застрявшим задачам, и поведение под сотней одновременных загрузок измерено, а не предположено.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль)
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** конвейер ревью переехал в плагин `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](../../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](../../review.md): типовые узлы, ложноположительные, вопросы к
|
||||
проходам, триггеры профиля, недоступное проверке.
|
||||
|
||||
Два прохода плагин при этом **упразднил**, и это надо помнить:
|
||||
|
||||
- `idiom` — поимённая сверка со стайлгайдами языка не задаётся теперь ни одним
|
||||
проходом; способные части переселены в `ops` и `architecture`. Класс
|
||||
обратимый (портит форму кода, не данные) и признаётся в границах покрытия.
|
||||
- `negative` — вопрос «что опытный человек отсюда удалил бы» вошёл в
|
||||
`architecture` вторым обязательным.
|
||||
|
||||
## Осталось
|
||||
|
||||
- **Ревьювер наименований** (соответствие словарю единого языка) — отдельной
|
||||
оптикой не выделен: зависит от задачи «Словарь единого языка», без глоссария
|
||||
проверять не по чему. Завести после неё.
|
||||
- **Калибровка проходов** по процедуре `references/calibration.md` скилла
|
||||
`av-dev-pipeline:review-pipeline` — ни один проход ещё не замерен инъекцией.
|
||||
До замера ничего не удаляем и промпты не правим.
|
||||
- **Заполнить журнал дефектов** в [docs/review.md](../../review.md) случаями,
|
||||
которые уже проскочили ревью, — они станут первыми пробами калибровки.
|
||||
- **Решить судьбу упразднённых проходов:** нужен ли проекту свой `idiom` поверх
|
||||
плагина, или записи в «Недоступно проверке» достаточно.
|
||||
|
||||
Связано: CLAUDE.md (ревью-процесс, конвенции),
|
||||
[docs/conventions/](../../conventions/README.md), «Словарь единого языка».
|
||||
@@ -0,0 +1,11 @@
|
||||
# [goal] Точность распознавания
|
||||
|
||||
- **Секция:** темы
|
||||
- **Зачем:** смена модели или правка промпта сегодня вслепую — нет ни метрики, ни способа переиспользовать уже сделанные человеком правки
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: распознавание — единственное место, где система может ошибиться молча и правдоподобно. Сегодня её точность не измеряется ничем, кроме впечатления, а накопленные правки человека пропадают.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда точность распознавания меряется числом на фиксированном корпусе реальных раздач, смена модели или правка промпта прогоняются через этот корпус до выкатки, а решение auto/review опирается на измеримую силу совпадения, а не на самооценку модели.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Eval-харнес распознавания (корпус кейсов + метрика точности)
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** смена модели или правка промпта распознавания сейчас вслепую — нет корпуса кейсов и метрики точности, регрессии не видно
|
||||
- **Теги:** goal:recognition-accuracy
|
||||
|
||||
Распознавание — ядро продукта, но смена модели или правка промпта сейчас вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по нему с метрикой точности (тип/название/год/нумерация). Тогда можно сравнивать LLM-провайдеры и версии промпта по числам. Прогон — отдельной командой (jellybit eval или тестом), на фикстурах, без реального qBittorrent.
|
||||
|
||||
Связано: specs/recognition.md (конвейер, модель уверенности), пакет recognize.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Полный редактор маппинга «файл → серия» и ручной режим ревью
|
||||
|
||||
- **Секция:** Ядро продукта
|
||||
- **Зачем:** правка 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 проверяемых утверждений списком, у каждого назван оракул -->
|
||||
|
||||
## Рамки
|
||||
|
||||
<!-- одна строка: чего касаться нельзя, что перезапускается, что считается необратимым -->
|
||||
@@ -0,0 +1,9 @@
|
||||
# НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
|
||||
- **Теги:** goal:operational-resilience
|
||||
|
||||
Потолок по нагрузке нигде не зафиксирован: воркер, поллинг qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз». Записать в НФТ целевой ориентир — архитектура держит до 100 одновременных загрузок в работе (приём → распознавание → раскладка), план-максимум — 1000. Сама запись требования дешева и высокоценна: задаёт рамку для решений ниже. Отдельно (дороже) — аудит узких мест: одиночное соединение SQLite и сериализация записи, конкурентность воркера и лимит параллельных распознаваний, частота/стоимость поллинга и дедуп при наплыве.
|
||||
|
||||
Связано: specs/architecture.md → «Отслеживание загрузки»/«Хранилище», пакеты worker, store, qbt, llm.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Бэкап SQLite
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** architecture требует бекапить data-том, но стратегия не описана — сбой или редеплой стирают всё in-flight состояние (проще, пока БД маленькая)
|
||||
- **Теги:** goal:operational-resilience
|
||||
|
||||
architecture.md требует «бекапить data-том», но как — не описано. Без понятной стратегии сбой или редеплой стирают всё in-flight состояние. Зафиксировать решение и реализовать: периодический VACUUM INTO в /data/backups по расписанию (с ротацией) либо потоковая репликация (litestream). Лучше сделать, пока БД маленькая.
|
||||
|
||||
Связано: specs/architecture.md → «Деплой» (data-том), пакет store.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Мгновенные обновления через SSE
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** живые обновления на 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.
|
||||
@@ -0,0 +1,11 @@
|
||||
# [goal] Целостность состояния и приёма
|
||||
|
||||
- **Секция:** темы
|
||||
- **Зачем:** известные окна рассинхрона и потери маркеров: каждое по отдельности самоисцеляется, вместе — источник необъяснимых состояний
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: состояние загрузки — то, по чему судят обо всём остальном. Накопились известные щели: окно namer'а, идентичность split v1/v2, потеря маркера dismiss, отсутствие истории переходов.
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда по записи загрузки можно ответить «как она сюда попала», ни один известный сегодня путь не оставляет состояние, которое не объясняется историей переходов, и идентичность раздачи не подделывается входом.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Ревью уведомлений в Telegram (аудит текстов и формата)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
|
||||
- **Теги:** 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).
|
||||
@@ -0,0 +1,9 @@
|
||||
# Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** По калибровке болей (2026-07-02) — не боль, из приоритета выпало
|
||||
- **Теги:** goal:complex-releases
|
||||
|
||||
По калибровке болей (2026-07-02) — не боль, из приоритета выпало. Сосуществование версий доступно уже сейчас (Jellyfin multi-version, другой целевой путь), коллизия на тот же путь штатно уходит в review. Явный replace (undo старого хардлинка → lay нового → супересид владения путём) — отдельный change, если/когда станет болью.
|
||||
|
||||
Связано: «Раздачи с докачиванием», specs/jellyfin-layout.md (never-overwrite, коллизия).
|
||||
@@ -0,0 +1,20 @@
|
||||
# Фетч .torrent по URL — остаток «единого окна»
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** 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`.
|
||||
|
||||
Связано: specs/architecture.md → «Транспорты» (source_type = magnet|torrent|url уже в
|
||||
схеме), пакет ingest, архив change `torrent-file-ingest`.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Словарь единого языка (ubiquitous language)
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
|
||||
- **Теги:** goal:dev-process-quality
|
||||
|
||||
Свести термины домена в один глоссарий, чтобы пользователь, документация, код и агент говорили на одном языке: загрузка, раздача, распознавание, матч, кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и т.д. — русский термин, английский идентификатор в коде, краткое определение. Сейчас наименования расходятся между спеками, UI и кодом. Глоссарий — источник истины по именам; на нём же строится агент-ревьювер наименований.
|
||||
|
||||
Связано: docs/conventions, specs/architecture.md, новый файл-глоссарий.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Авторизация веб-UI (на будущее)
|
||||
|
||||
- **Секция:** инфраструктура
|
||||
- **Зачем:** для v1 решено без авторизации (доверенная LAN, опц. allowlist подсетей) — задел на случай, если понадобится защита
|
||||
- **Теги:** goal:ingest-and-review-interfaces
|
||||
|
||||
Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей (http.trusted_subnets) — как умеет qBittorrent. Если понадобится защита: токен/Basic в самом приложении или вынос за reverse-proxy с аутентификацией.
|
||||
|
||||
Связано: specs/architecture.md → «Транспорты» (доступ к веб-UI), пакет httpapi.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Современный Web-UI как PWA
|
||||
|
||||
- **Секция:** ядро продукта
|
||||
- **Зачем:** текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
|
||||
- **Теги:** goal:ingest-and-review-interfaces
|
||||
|
||||
Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое, удобное с телефона). Текущий server-rendered UI функционален, поэтому это улучшение, а не блокер; большой объём работы.
|
||||
|
||||
Связано: specs/review-ux.md (веб = точные правки), пакет httpapi.
|
||||
Reference in New Issue
Block a user