# CLAUDE.md

Памятка для работы над jellybit. Перед задачей прочитай также
[README.md](README.md), [BRIEF.md](BRIEF.md) и
[docs/specs/architecture.md](docs/specs/architecture.md). Разработка идёт
по **Spec Driven Development** через OpenSpec — см. раздел ниже.

## Что это

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

## Стек и принципы

- **Go**, один статический бинарь (`CGO_ENABLED=0`). Почему — см.
  [ADR-2026-06-13-go-single-binary](docs/adr/ADR-2026-06-13-go-single-binary.md).
- **SQLite** как хранилище (чистый Go-драйвер `modernc.org/sqlite`).
- **Конфигурация — TOML**. **Логи — структурированный JSON** (`log/slog`).
- **Хардлинки, источник не трогаем** — qBittorrent продолжает раздачу,
  диск не дублируется.
- **Единое ядро, тонкие транспорты** — вся логика приёма в use-case
  `Ingest`; HTTP API, веб-UI и Telegram — лишь обёртки над ним.
- **Минимум компонентов** — в духе umbar, без зоопарка сервисов. Внешние
  базы метаданных (TMDB/TVDB) опциональны, включаются конфигом.

## Инварианты (безопасность данных)

- **Источник неприкосновенен:** только `mkdir` / `link(2)` / `unlink`
  своих ссылок; никогда не трогаем файлы под `paths.downloads`.
- **Целевой путь санитизируется** и проверяется, что он строго под
  `paths.movies`/`series` (защита от traversal); существующее не
  перезаписываем.
- **Выход LLM недоверенный** — безопасность на валидации пути, не на
  промпте. Авто-раскладка только при подтверждённом матче в базе.
- **Секреты не попадают в логи** — пароли qBittorrent, API-ключи LLM/метабаз,
  auth-заголовки. Подробнее — [docs/conventions/logging.md](docs/conventions/logging.md).
- **Запуск:** контейнер под `1000:1000`, в общей docker-сети (адресация
  по именам), mount `/srv/media` (единая песочница) + data-том для
  SQLite/конфига.

## Spec Driven Development (OpenSpec)

Изменения ведём через [OpenSpec](https://github.com/Fission-AI/OpenSpec)
(CLI `openspec`, v1.x). Сначала спецификация — потом код.

- `openspec/specs/<capability>/spec.md` — **актуальные** capability-спеки:
  что система делает сейчас. 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` — язык и правила оформления спек (читай перед
  написанием).

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

Правила спек:

- Каждое `### Requirement` ОБЯЗАНО содержать литерал `SHALL` или `MUST` —
  иначе `openspec validate` падает.
- Структурные заголовки и ключевые слова — английские (`### Requirement:`,
  `#### Scenario:`, `GIVEN/WHEN/THEN`, RFC 2119), остальной текст — русский.
- Сценарии — в формате `GIVEN/WHEN/THEN`.
- `openspec validate --strict` перед коммитом change.

Ревью (процесс, не артефакт): нетривиальная задача — два чекпоинта (ревью
дизайна после design/specs, ДО кода; ревью кода после apply, до archive);
тривиальная — одного прохода по коду достаточно.

**Миграция:** capabilities постепенно переносятся из `docs/specs/` в
OpenSpec (пилот — `ingest`). До переноса источник истины по теме —
соответствующий файл в `docs/specs/`; перенесённое живёт в
`openspec/specs/`.

## Прочая документация

- `docs/specs/` — **живые** спецификации целевого состояния (архитектурный
  обзор + ещё не перенесённые в OpenSpec темы). Меняем по мере развития,
  держим в соответствии с кодом.
- `docs/adr/` — **неизменяемый** журнал решений, пишется постфактум,
  хранит *почему*. Правила — [docs/adr/README.md](docs/adr/README.md).
- `docs/drafts/` — черновики: планы, идеи, ещё не принятые решения. Не
  источник истины.

## Задачи и беклог

- **Единственный источник беклога — каталог [docs/backlog/](docs/backlog/README.md)**:
  одна задача = один markdown-файл (`docs/backlog/<slug>.md`), плюс
  индекс [README.md](docs/backlog/README.md) со списком по приоритетам
  (высокий/средний/низкий) и хуками. В теле файла — контекст, принятые
  решения, шаги и ссылки на спеки/ADR/черновики. Заводи задачу новым файлом
  и строкой в индексе; закрытую (реализованную) — удаляй, суть переезжает в
  `docs/specs`/`docs/adr`.
- Спекулятивные задачи (ещё без решения «делаем») помечены префиксом
  `[идея]` в названии — их сперва прорабатываем.
- **Tududi — только инбокс сырых идей** (проект `jellybit`, project_id 14).
  Беклог там больше не ведём; идея из Tududi становится задачей, когда её
  оформляют файлом в `docs/backlog/`. Прежняя единая `docs/backlog.md`
  доступна в истории git.

## Язык

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

## Команды

Запуск через [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 tidy` — `go mod tidy`
- `task image` — docker-образ из готового бинаря

Module path — `git.vakhrushev.me/av/jellybit`. Go 1.26, `CGO_ENABLED=0`.
Стек: `chi`, `sqlx` + `modernc.org/sqlite`, `goose` (миграции),
`pelletier/go-toml/v2`, `log/slog`.

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

- Раскладка: `cmd/jellybit` (точка входа) + `internal/<пакет>` по
  компонентам из [architecture.md](docs/specs/architecture.md).
- Ошибки — stdlib, обёртка с контекстом (`fmt.Errorf("...: %w", err)`),
  проверка через `errors.Is`/`errors.As`, трансляция на внешней границе:
  [docs/conventions/errors.md](docs/conventions/errors.md).
- Логирование только через `slog`, без `fmt.Println` — уровни, обязательные
  поля и что не логировать см. [docs/conventions/logging.md](docs/conventions/logging.md).
- Конфигурация — только TOML; секреты рендерит деплой (Ansible+Vault) в
  файл (`config.toml` не коммитится, `0600`), не в env; валидация на старте:
  [docs/conventions/config.md](docs/conventions/config.md).
- Время — храним в UTC, RFC 3339 с суффиксом `Z`; генерирует только приложение
  (`store.Now()`), таймзона отображения — конфиг `[general].timezone`:
  [docs/conventions/database.md](docs/conventions/database.md).
- Идентификаторы — TEXT ULID (lowercase) через `internal/ident`, без числовых
  AUTOINCREMENT; внешние id валидируются `ident.Parse` на границе:
  [docs/conventions/database.md](docs/conventions/database.md).
- Миграции БД (goose, `internal/store/migrations`; SQL для DDL, Go — когда
  нужен код) — при изменении структуры (таблица/столбец/индекс/связь) в том же
  change обновляем ER-схему [docs/specs/database.md](docs/specs/database.md).
- Веб-UI на htmx — единый партиал = страница = фрагмент, ветвление по `isHTMX`,
  деградация без JS, ошибка на htmx-пути = 200 + фрагмент, самозавершающийся
  поллинг: [docs/conventions/web-ui.md](docs/conventions/web-ui.md).

Кросс-каттинг конвенции (как пишем код, а не что система делает) живут в
[docs/conventions/](docs/conventions/README.md) и не переносятся в OpenSpec.
