Files
jellybit/CLAUDE.md
T
av c5d62d76ee docs: канон поднят с версии 7 до 12
- каталог задач переехал в tasks/ в корне, спринт упразднён — приоритет
  теперь порядок строк в BACKLOG.md, четыре задачи набора вернулись в беклог
- гейт: путь docs.py переведён на av-dev-docs вместо снесённого av-dev-pm,
  добавлены шаги tasks.py check и openspec.py check
- относительные ссылки внутри задач и ссылки из docs/ на задачи починены
2026-08-09 19:09:53 +03:00

238 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`, разбор `.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](openspec/specs/state-reconciliation/spec.md));
(2) уборка воркером **собственного** торрента, добавленного этим же `add`
секундами ранее, когда закрытие **любым** путём (`Cancel` или `Dismiss`)
увело задачу из `catched` в окне после `add` — уборка привязана к состоянию,
а не к команде; признак «своё» даёт подтверждённое отсутствие инфохэша
непосредственно перед `add`
([download-tracking](openspec/specs/download-tracking/spec.md),
[state-reconciliation](openspec/specs/state-reconciliation/spec.md)). Всё
остальное под `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](docs/adr/ADR-2026-06-13-auto-link-requires-db-match.md),
[recognition](openspec/specs/recognition/spec.md)). Обратимо
через `Undo`. **`major`.**
- **Не более одной активной загрузки на infohash** — проверка отсутствия другой
активной загрузки и вставка идут одной write-транзакцией (`_txlock=immediate`,
guarded-методы `store`); обход даёт две задачи, претендующие на одну раздачу и
один целевой путь. Обратимо (лишняя закрывается), но состояние расходится.
**`major`.** Поведение — [ingest](openspec/specs/ingest/spec.md).
- **Переходы состояний — только через `worker` под per-download блокировкой**,
и только легальные по декларативному графу. Обход даёт гонку двух
транспортов. **`major`.**
- **Время — только `store.Now()` (UTC), идентификаторы — только `ident`**;
`ident.Parse` на каждой входной границе. Время механизировано линтером
(`forbidigo` на `time.Now`); правило про `ident` линтером не проверяется —
держится на ревью. **`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` с явным «гонки НЕ проверены» — тогда их
проверяет рассуждением тема `operations` ([docs/review.md](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](tasks/BACKLOG.md).**
Первая строка секции — то, что делают следующим. Порядок назначает человек на
груминге (`av-dev-tasks:groom`), машина его не выводит.
- **Ориентир по размеру порции разбора на груминге:** 5–8 задач. Ориентир, а не
закон.
- **Что такое «сделана»:** пайплайн задачи пройден целиком (спека → код → оба
чекпоинта ревью → archive) и критерии приёмки проверены поимённо.
## Spec Driven Development (OpenSpec)
Изменения ведём через [OpenSpec](https://github.com/Fission-AI/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](openspec/config.yaml) (`context` и `rules`), оттуда их
читает порождение артефактов; здесь не дублируются. Перед коммитом change —
`openspec validate --strict`.
Ревью — два чекпоинта: ревью дизайна на предложении (после design/specs, ДО
кода) и ревью изменения после apply, до archive. Состав обоих выбирается по
метке задачи (`small` / `medium` / `large`), которую разметка ставит один раз
после propose. Настройка конвейера под проект и журнал дефектов —
[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) — настройка конвейера ревью и журнал дефектов.
- [tasks/](tasks/BACKLOG.md) — задачи и цели: одна запись = один файл
в `items/` + строка в индексе, порядок строк = приоритет. Ведётся скиллом
`av-dev-tasks:tasks`, разбор беклога — `av-dev-tasks:groom`.
**Tududi** (проект `jellybit`, project_id 14) — только инбокс сырых идей. Идея
становится задачей, когда её оформляют файлом в `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` гейта.
## Язык
- Документация, комментарии, сообщения коммитов — **русский**.
- Код и идентификаторы — английский.