Files
jellybit/CLAUDE.md
T
av 69853a96c9 docs: разобран урожай судей после подъёма канона
- перечень команд пользователя убран из architecture.md в спеки review и
  state-reconciliation, где ему дом: обзор успел разойтись с ними в обе стороны
- README перестал дублировать деплой и статус — теперь ссылается на дом
- статус «заведена ли задача под пробел» сведён в один регистр открытых вопросов
- шаги tasks.py и openspec.py названы в перечне «что красит безусловно»
- из спеки download-tracking сняты числа умолчаний: их дом — database.md
2026-08-09 19:24:59 +03:00

19 KiB
Raw Blame History

CLAUDE.md

Памятка для работы над jellybit. Перед задачей прочитай также docs/passport.md, docs/architecture.md и docs/conventions/. Разработка идёт по Spec Driven Development через OpenSpec — см. раздел ниже.

Что это

Связующий сервис qBittorrent ↔ Jellyfin: принимает торрент с текстовым контекстом, качает через qBittorrent, распознаёт фильм или сериал (LLM + контекст + опц. метабазы) и раскладывает файлы для Jellyfin хардлинками, не трогая исходную раздачу. Деплоится на домашний медиа-сервер umbar (/home/av/projects/private/umbar).

Чего не делает: не ищет раздачи в трекерах, не ведёт профили качества, не подписывается на выходящие серии, не хранит медиа и не заменяет Jellyfin. Полная граница домена — docs/passport.md.

Стек

Go 1.26, один статический бинарь (CGO_ENABLED=0). Module path — git.vakhrushev.me/av/jellybit. SQLite через modernc.org/sqlite + sqlx, миграции goose, HTTP — chi + html/template + htmx, конфиг — pelletier/go-toml/v2, разбор .torrent и инфохэшей — anacrolix/torrent, пред-парс имени раздачи — middelink/go-parse-torrent-name, логи — log/slog (структурированный JSON).

Инварианты

Нарушать нельзя. Severity стоит здесь, а не выводится каждым проходом ревью заново.

  • Источник неприкосновенен — под paths.downloads допустимы только чтение и link(2); никаких unlink, rename, записи. Нарушение уничтожает невосстановимые данные пользователя. Необратимо. critical. Исключения два, и оба — не наши операции с файловой системой, а вызов torrents/delete qBittorrent с deleteFiles=true: (1) Delete из done/orphaned/target_missing по явному подтверждению человека — гард последней копии там выключен сознательно (state-reconciliation); (2) уборка воркером собственного торрента, добавленного этим же add секундами ранее, когда закрытие любым путём (Cancel или Dismiss) увело задачу из catched в окне после add — уборка привязана к состоянию, а не к команде; признак «своё» даёт подтверждённое отсутствие инфохэша непосредственно перед add (download-tracking, state-reconciliation). Всё остальное под paths.downloads — по-прежнему critical.
  • Последняя копия не снимаетсяUndo отклоняется целиком, если у цели не осталось других жёстких ссылок (nlink <= 1) или исходного файла уже нет. Частичный откат тоже стёр бы часть данных. Необратимо. critical.
  • Целевой путь строго под библиотекой — после санитизации и filepath.Clean путь обязан лежать под paths.movies/paths.series, иначе операция отклоняется. Выход за песочницу означает запись в чужие каталоги. Необратимо. critical. Выход LLM недоверенный: безопасность держится на этой проверке, а не на промпте.
  • Существующее не перезаписываем — цель занята другим файлом → коллизия → review. Обратимо (задача уходит в ревью), но потеря чужого файла — нет. critical.
  • Секреты не попадают в логи, диагностику и ответы API — пароль qBittorrent, ключи LLM и метабаз, токен Telegram, API-ключ Jellyfin. Утёкший в лог секрет отзывается вручную. major.
  • Авто-раскладка только при подтверждённом матче в метабазе — самооценка LLM единственным гейтом не является и матч не заменяет: порог [recognition].auto_confidence_threshold стоит поверх матча дополнительным условием (ADR, recognition). Обратимо через Undo. major.
  • Не более одной активной загрузки на infohash — проверка отсутствия другой активной загрузки и вставка идут одной write-транзакцией (_txlock=immediate, guarded-методы store); обход даёт две задачи, претендующие на одну раздачу и один целевой путь. Обратимо (лишняя закрывается), но состояние расходится. major. Поведение — ingest.
  • Переходы состояний — только через worker под per-download блокировкой, и только легальные по декларативному графу. Обход даёт гонку двух транспортов. major.
  • Время — только store.Now() (UTC), идентификаторы — только ident; ident.Parse на каждой входной границе. Время механизировано линтером (forbidigo на time.Now); правило про ident линтером не проверяется — держится на ревью. minor.

Команды

Запуск через Task (task --list — полный список):

  • task setup — установка тулинга (golangci-lint + git-хуки lefthook)
  • task run — локальный запуск (go run ./cmd/jellybit --config ./config.toml)
  • task build — статический бинарь linux/amd64 для сервера
  • task test / task lint — тесты и golangci-lint
  • task gate — детерминированный гейт ревью (см. ниже)
  • task review:context — карта проекта для архитектурного прохода ревью
  • task tidygo mod tidy
  • task image — docker-образ из готового бинаря

Гейт

  • Команда целиком: task gate (BASE=<rev> задаёт базу диффа). Без BASE база — git merge-base HEAD master, а на самом masterHEAD~1.
  • Где логи шагов: tmp/gate/<шаг>.log, по одному файлу на шаг.
  • Что означает исход: статусы OK / FAIL (краснит) / WARN (виден, не блокирует) / SKIP (не применим, всегда с причиной). Код возврата 1, если есть хоть один FAIL. Гейт не останавливается на первом отказе — ревьюверу нужна полная картина.
  • Что красит безусловно: сборка, go vet, golangci-lint, gofmt, тесты, флаки-прогон (второй прогон разошёлся с первым), -race, накат миграций с нуля, gitleaks, канон документации (docs.py check — раскладка docs/, битые ссылки, «миграция изменена, а database.md нет»), каталог задач (tasks.py check --dir tasks — согласованность tasks/BACKLOG.md и tasks/items/), форма конфига OpenSpec (openspec.py check — незаменённый пример в openspec/config.yaml). Причина одна: у каждого из них есть объективный оракул, спорить не о чем. Каждый из трёх последних краснеет и когда своего скрипта нет: молча пропущенная проверка неотличима от пройденной.
  • Чего в гейте намеренно нет и кто обязан это гонять:
    • govulncheck даёт WARN, а не FAIL: находка тут — состояние зависимостей, а не диффа. Разбирает агент ревью по трассам вызовов.
    • -race без gcc уходит в SKIP с явным «гонки НЕ проверены» — тогда их проверяет рассуждением тема operations (docs/review.md → «Вопросы по темам»), и это идёт в границы покрытия.
    • Ничего не гоняется против живого qBittorrent, LLM и метабаз: интеграционные тесты за env-гейтами, запускает человек вручную.
    • Качество распознавания гейтом не проверяется вовсе и проверяться не будет: размеченный корпус решено не собирать (tasks/REJECTED.md, 2026-08-06). Сдвиг точности виден только по рабочему потоку.

Запреты

  • Не запускать против рабочей БД /data/jellybit.db на umbar и против любого файла, на который указывает боевой [storage].db_path. Локально — только ./jellybit.db.
  • Не писать в /srv/media/downloads и вообще никуда под paths.downloads: там живут раздачи, которые qBittorrent продолжает сидировать.
  • Не ходить в боевой qBittorrent, Jellyfin и Telegram-бота из тестов и отладочных прогонов. Интеграционные тесты — за env-гейтами (*_integration_test.go), включает человек осознанно.
  • Не расходовать лимиты метабаз и платного LLM прогонами «посмотреть, что будет»: у recognize --dry-run есть цена.
  • testdata отдельным каталогом не заводился: фикстуры чужих форматов живут константами в тестах пакета-разборщика (internal/tgbot/parse_test.go, internal/magnet, internal/torrent).
  • Временное — только в tmp/.gitignore); туда же пишет гейт. Не в /tmp, не рядом с исходниками.

Работа

  • Основная ветка: master. От неё считается база диффа (git merge-base HEAD master), в неё вливает батч, от неё ветвятся задачи.
  • Необратимое (спрашивается у человека всегда): всё, что пишет в paths.downloads или удаляет оттуда; удаление раздачи из qBittorrent вместе с файлами (Delete) — кроме уборки собственного, только что добавленного торрента, когда закрытие любым путём увело задачу из catched (см. исключения инварианта выше); снятие последней копии данных; правка уже применённой миграции; git push --force; удаление или перезапись файла в библиотеке Jellyfin, которого мы не создавали.
  • Что считается сломанным: покрасневший task gate на master. Пока он красный, ни одна задача не считается сделанной, и чинится он вперёд любой задачи — станок общий.
  • Приоритет — это порядок строк в tasks/BACKLOG.md. Первая строка секции — то, что делают следующим. Порядок назначает человек на груминге (av-dev-tasks:groom), машина его не выводит.
  • Ориентир по размеру порции разбора на груминге: 5–8 задач. Ориентир, а не закон.
  • Что такое «сделана»: пайплайн задачи пройден целиком (спека → код → оба чекпоинта ревью → archive) и критерии приёмки проверены поимённо.

Spec Driven Development (OpenSpec)

Изменения ведём через OpenSpec (CLI openspec, v1.x). Сначала спецификация — потом код.

  • openspec/specs/<capability>/spec.mdнормативный дом поведения: что система делает сейчас. Capability — это поведение или домен системы, а не пакет кода.
  • openspec/changes/<id>/ — предлагаемое изменение: proposal.md, design.md (для нетривиальных), дельта-спеки (ADDED/MODIFIED/REMOVED Requirements), tasks.md. После реализации change архивируется в openspec/changes/archive/, дельты вливаются в openspec/specs/.
  • openspec/config.yaml — только нужды генерации артефактов: язык, правила именования capability, придирки валидатора — плюс адреса документов канона. Пересказа этих документов там нет: второй дом факта расходится молча.

Поток работы — через слэш-команды opsx:*: opsx:explore (продумать), opsx:propose (завести change), opsx:apply (реализовать tasks), opsx:sync/opsx:archive (влить и архивировать).

Правила спек — язык, именование capability и придирки валидатора — живут в openspec/config.yaml (context и rules), оттуда их читает порождение артефактов; здесь не дублируются. Перед коммитом change — openspec validate --strict.

Ревью — два чекпоинта: ревью дизайна на предложении (после design/specs, ДО кода) и ревью изменения после apply, до archive. Состав обоих выбирается по метке задачи (small / medium / large), которую разметка ставит один раз после propose. Настройка конвейера под проект и журнал дефектов — docs/review.md.

Документация

Раскладка задана каноном av-dev; проверяет её docs.py check внутри task gate.

  • docs/passport.md — зачем и для кого, чем не является.
  • docs/architecture.md — обзор, эксплуатация, единые точки, деплой. Поведения здесь нет — оно в openspec/specs/.
  • docs/database.md — схема, представление данных, настройки с числовым значением.
  • docs/security.md — периметр, недоверенный вход, что вне модели.
  • docs/conventions/ — как пишем код.
  • docs/research/ — наблюдения за чужими форматами.
  • docs/adr/ — журнал решений, неизменяемый.
  • docs/review.md — настройка конвейера ревью и журнал дефектов.
  • tasks/ — задачи и цели: одна запись = один файл в items/ + строка в индексе, порядок строк = приоритет. Ведётся скиллом av-dev-tasks:tasks, разбор беклога — av-dev-tasks:groom.

Tududi (проект jellybit, project_id 14) — только инбокс сырых идей. Идея становится задачей, когда её оформляют файлом в tasks/items/.

Конвенции кода

  • Раскладка: cmd/jellybit (точка входа) + internal/<пакет> по компонентам из docs/architecture.md.
  • Механизируемое проверяет task gate (.golangci.yml + internal/archrules): форма ошибок и логов, конфиг мимо env, время мимо store.Now(), AUTOINCREMENT в миграциях, направление зависимостей ядро↔транспорты. Перечень с местом механизации — docs/conventions/README.md; пересказывать эти правила прозой не нужно.
  • Прозой остаётся только то, что правилом не выражается, и читается в источнике: ошибки, логи, конфиг, БД, веб-UI.
  • Миграции БД (goose, internal/store/migrations; SQL для DDL, Go — когда нужен код): при изменении структуры в том же change обновляем ER-схему в docs/database.md — иначе краснеет шаг canon гейта.

Язык

  • Документация, комментарии, сообщения коммитов — русский.
  • Код и идентификаторы — английский.