# CLAUDE.md Памятка для работы над jellybit. Перед задачей прочитай также [docs/passport.md](docs/passport.md), [docs/architecture.md](docs/architecture.md) и [docs/conventions/](docs/conventions/README.md). Разработка идёт по **Spec Driven Development** через OpenSpec — см. раздел ниже. ## Что это Связующий сервис qBittorrent ↔ Jellyfin: принимает торрент с текстовым контекстом, качает через qBittorrent, распознаёт фильм или сериал (LLM + контекст + опц. метабазы) и раскладывает файлы для Jellyfin хардлинками, не трогая исходную раздачу. Деплоится на домашний медиа-сервер umbar (`/home/av/projects/private/umbar`). **Чего не делает:** не ищет раздачи в трекерах, не ведёт профили качества, не подписывается на выходящие серии, не хранит медиа и не заменяет Jellyfin. Полная граница домена — [docs/passport.md](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`, логи — `log/slog` (структурированный JSON). ## Инварианты Нарушать нельзя. Severity стоит здесь, а не выводится каждым проходом ревью заново. - **Источник неприкосновенен** — под `paths.downloads` допустимы только чтение и `link(2)`; никаких `unlink`, `rename`, записи. Нарушение уничтожает невосстановимые данные пользователя. **Необратимо. `critical`.** - **Последняя копия не снимается** — `Undo` отклоняется целиком, если у цели не осталось других жёстких ссылок (`nlink <= 1`) или исходного файла уже нет. Частичный откат тоже стёр бы часть данных. **Необратимо. `critical`.** - **Целевой путь строго под библиотекой** — после санитизации и `filepath.Clean` путь обязан лежать под `paths.movies`/`paths.series`, иначе операция отклоняется. Выход за песочницу означает запись в чужие каталоги. **Необратимо. `critical`.** Выход LLM недоверенный: безопасность держится на этой проверке, а не на промпте. - **Существующее не перезаписываем** — цель занята другим файлом → коллизия → review. Обратимо (задача уходит в ревью), но потеря чужого файла — нет. **`critical`.** - **Секреты не попадают в логи, диагностику и ответы API** — пароль qBittorrent, ключи LLM и метабаз, токен Telegram, API-ключ Jellyfin. Утёкший в лог секрет отзывается вручную. **`major`.** - **Авто-раскладка только при подтверждённом матче в метабазе** — самооценка LLM гейтом не является ([ADR](docs/adr/ADR-2026-06-13-auto-link-requires-db-match.md)). Обратимо через `Undo`. **`major`.** - **Переходы состояний — только через `worker` под per-download блокировкой**, и только легальные по декларативному графу. Обход даёт гонку двух транспортов. **`major`.** - **Время — только `store.Now()` (UTC), идентификаторы — только `ident`**; `ident.Parse` на каждой входной границе. Механизировано линтером. **`minor`.** ## Команды Запуск через [Task](https://taskfile.dev) (`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 tidy` — `go mod tidy` - `task image` — docker-образ из готового бинаря ## Гейт - **Команда целиком:** `task gate` (`BASE=` задаёт базу диффа). Без `BASE` база — `git merge-base HEAD master`, а на самом `master` — `HEAD~1`. - **Где логи шагов:** `tmp/gate/<шаг>.log`, по одному файлу на шаг. - **Что означает исход:** статусы `OK` / `FAIL` (краснит) / `WARN` (виден, не блокирует) / `SKIP` (не применим, всегда с причиной). Код возврата 1, если есть хоть один `FAIL`. Гейт **не** останавливается на первом отказе — ревьюверу нужна полная картина. - **Что красит безусловно:** сборка, `go vet`, `golangci-lint`, `gofmt`, тесты, флаки-прогон (второй прогон разошёлся с первым), `-race`, накат миграций с нуля, `gitleaks`, канон документации (`docs.py check` — раскладка `docs/`, битые ссылки, «миграция изменена, а `database.md` нет»). Причина одна: у каждого из них есть объективный оракул, спорить не о чем. - **Чего в гейте намеренно нет и кто обязан это гонять:** - `govulncheck` даёт `WARN`, а не `FAIL`: находка тут — состояние зависимостей, а не диффа. Разбирает агент ревью по трассам вызовов. - `-race` без gcc уходит в `SKIP` с явным «гонки НЕ проверены» — тогда их проверяет проход `ops` рассуждением, и это идёт в границы покрытия. - Ничего не гоняется против **живого** qBittorrent, LLM и метабаз: интеграционные тесты за env-гейтами, запускает человек вручную. - Качество распознавания гейтом не проверяется вовсе — нужен корпус кейсов (задача `recognition-eval-harness`). ## Запреты - **Не запускать против рабочей БД** `/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`); снятие последней копии данных; правка уже применённой миграции; `git push --force`; удаление или перезапись файла в библиотеке Jellyfin, которого мы не создавали. - **Общий станок** — покрасневший `task gate` на `master` врывается в замороженный спринт: пока он красный, ни одна задача не считается сделанной. - **Ориентир по размеру спринта:** 5–8 задач. Ориентир, а не закон. - **Что такое «сделана»:** пайплайн задачи пройден целиком (спека → код → оба чекпоинта ревью → archive) и критерии приёмки проверены поимённо. ## Spec Driven Development (OpenSpec) Изменения ведём через [OpenSpec](https://github.com/Fission-AI/OpenSpec) (CLI `openspec`, v1.x). Сначала спецификация — потом код. - `openspec/specs//spec.md` — **нормативный дом поведения**: что система делает сейчас. Capability — это поведение или домен (`ingest`, `recognition`, `file-layout`, `review`, `notifications`), а не пакет кода. - `openspec/changes//` — предлагаемое изменение: `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` (влить и архивировать). Правила спек: - Каждое `### Requirement` ОБЯЗАНО содержать литерал `SHALL` или `MUST` — иначе `openspec validate` падает. - Структурные заголовки и ключевые слова — английские (`### Requirement:`, `#### Scenario:`, `GIVEN/WHEN/THEN`, RFC 2119), остальной текст — русский. - `openspec validate --strict` перед коммитом change. Ревью — два чекпоинта: профиль `design` на предложении (после design/specs, ДО кода) и ревью изменения после apply, до archive. Настройка конвейера под проект и журнал дефектов — [docs/review.md](docs/review.md). ## Документация Раскладка задана каноном av-dev; проверяет её `docs.py check` внутри `task gate`. - [docs/passport.md](docs/passport.md) — зачем и для кого, чем **не** является. - [docs/architecture.md](docs/architecture.md) — обзор, эксплуатация, единые точки, деплой. **Поведения здесь нет** — оно в `openspec/specs/`. - [docs/database.md](docs/database.md) — схема, представление данных, настройки с числовым значением. - [docs/security.md](docs/security.md) — периметр, недоверенный вход, что вне модели. - [docs/conventions/](docs/conventions/README.md) — как пишем код. - [docs/research/](docs/research/README.md) — наблюдения за чужими форматами. - [docs/adr/](docs/adr/README.md) — журнал решений, неизменяемый. - [docs/review.md](docs/review.md) — настройка конвейера ревью и журнал дефектов. - [docs/tasks/](docs/tasks/BACKLOG.md) — задачи и цели: одна запись = один файл в `items/` + строка в индексе. Ведётся скиллом `av-dev-pm:tasks`, ритуал спринта — `av-dev-pm:session`. **Tududi** (проект `jellybit`, project_id 14) — только инбокс сырых идей. Идея становится задачей, когда её оформляют файлом в `docs/tasks/items/`. ## Конвенции кода - Раскладка: `cmd/jellybit` (точка входа) + `internal/<пакет>` по компонентам из [docs/architecture.md](docs/architecture.md). - **Механизируемое проверяет `task gate`** (`.golangci.yml` + `internal/archrules`): форма ошибок и логов, конфиг мимо env, время мимо `store.Now()`, `AUTOINCREMENT` в миграциях, направление зависимостей ядро↔транспорты. Перечень с местом механизации — [docs/conventions/README.md](docs/conventions/README.md); пересказывать эти правила прозой не нужно. - Прозой остаётся только то, что правилом не выражается, и читается в источнике: [ошибки](docs/conventions/errors.md), [логи](docs/conventions/logging.md), [конфиг](docs/conventions/config.md), [БД](docs/conventions/database.md), [веб-UI](docs/conventions/web-ui.md). - Миграции БД (goose, `internal/store/migrations`; SQL для DDL, Go — когда нужен код): при изменении структуры в том же change обновляем ER-схему в [docs/database.md](docs/database.md) — иначе краснеет шаг `canon` гейта. ## Язык - Документация, комментарии, сообщения коммитов — **русский**. - Код и идентификаторы — английский.