- Раскладка 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.
204 lines
15 KiB
Markdown
204 lines
15 KiB
Markdown
# 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=<rev>` задаёт базу диффа). Без `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/<capability>/spec.md` — **нормативный дом поведения**: что
|
||
система делает сейчас. Capability — это поведение или домен (`ingest`,
|
||
`recognition`, `file-layout`, `review`, `notifications`), а не пакет кода.
|
||
- `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` (влить и архивировать).
|
||
|
||
Правила спек:
|
||
|
||
- Каждое `### 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` гейта.
|
||
|
||
## Язык
|
||
|
||
- Документация, комментарии, сообщения коммитов — **русский**.
|
||
- Код и идентификаторы — английский.
|