docs: перевод документации на канон av-dev

- Раскладка 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.
This commit is contained in:
av
2026-08-04 09:27:26 +03:00
parent 08bef2cac0
commit 42d5b73a04
128 changed files with 1606 additions and 4889 deletions
+173 -146
View File
@@ -1,130 +1,60 @@
# CLAUDE.md
Памятка для работы над jellybit. Перед задачей прочитай также
[README.md](README.md), [BRIEF.md](BRIEF.md) и
[docs/specs/architecture.md](docs/specs/architecture.md). Разработка идёт
по **Spec Driven Development** через OpenSpec — см. раздел ниже.
[docs/passport.md](docs/passport.md), [docs/architecture.md](docs/architecture.md)
и [docs/conventions/](docs/conventions/README.md). Разработка идёт по **Spec
Driven Development** через OpenSpec — см. раздел ниже.
## Что это
Связующий сервис qBittorrent ↔ Jellyfin: принимает торрент + контекст,
качает, распознаёт фильм/сериал (LLM + контекст + опц. метабазы) и
раскладывает файлы для Jellyfin хардлинками. Деплоится на домашний
медиа-сервер umbar (`/home/av/projects/private/umbar`) — туда копируется
готовый бинарь.
Связующий сервис qBittorrent ↔ Jellyfin: принимает торрент с текстовым
контекстом, качает через qBittorrent, распознаёт фильм или сериал (LLM +
контекст + опц. метабазы) и раскладывает файлы для Jellyfin хардлинками, не
трогая исходную раздачу. Деплоится на домашний медиа-сервер umbar
(`/home/av/projects/private/umbar`).
## Стек и принципы
**Чего не делает:** не ищет раздачи в трекерах, не ведёт профили качества, не
подписывается на выходящие серии, не хранит медиа и не заменяет Jellyfin.
Полная граница домена — [docs/passport.md](docs/passport.md).
- **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) опциональны, включаются конфигом.
## Стек
## Инварианты (безопасность данных)
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).
- **Источник неприкосновенен:** только `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)
Нарушать нельзя. Severity стоит здесь, а не выводится каждым проходом ревью
заново.
Изменения ведём через [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` на предложении
(после design/specs, ДО кода) и ревью изменения после apply, до archive. Оба
идут через скилл `.claude/skills/review-pipeline`: детерминированный гейт
(`task gate`) → сверка с дельта-спеками в обе стороны → generative-проходы →
триаж с потолком 7 находок. Профиль (`quick`/`standard`/`deep`) выбирается по
факту изменения, правило — в скилле.
**Миграция:** 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). Приоритеты: высокий/средний/низкий.
Работа с беклогом (заведение из диалога, разбор находок ревью/аудита, груминг,
приоритизация, декомпозиция, штурм идеи) — через скилл **`backlog`**; формат
файла, слага, индекса и кладбища он и держит, здесь не дублируем. Скилл
поставляется плагином `av-dev-backlog` (маркетплейс `av-dev-skills`, включён в
`.claude/settings.json`); вызов — `/av-dev-backlog:backlog`, свой скрипт
`backlog.py` он зовёт сам — путь к нему в проекте не зашиваем.
- **Источники задач:** диалог, инбокс Tududi и находки ревью. Отложенная находка
`review-pipeline` (реальная, но не для текущего мерджа) заводится задачей через
скилл `backlog` с тегом партии `review-ГГГГ-ММ-ДД` — так уже сделаны задачи
`review-*` в беклоге.
- **Проектные тонкости для скилла `backlog`:**
- каталог беклога — `docs/backlog/`, язык — русский, слаги — латиница;
- реализованная задача удаляется, её суть переезжает в `docs/specs`/`docs/adr`
(это делает пайплайн задачи на шаге 9, не скилл беклога);
- выкинутая без реализации уезжает строкой в `docs/backlog/CLOSED.md` (кладбище);
- спекулятивные задачи помечены `[idea]` в заголовке (тип — английское
ключевое слово idea/epic/task) — сперва штурм.
- **Tududi — только инбокс сырых идей** (проект `jellybit`, project_id 14).
Беклог там больше не ведём; идея из Tududi становится задачей, когда её
оформляют файлом в `docs/backlog/` через скилл `backlog`. Прежняя единая
`docs/backlog.md` доступна в истории git.
## Язык
- Документация, комментарии, сообщения коммитов — **русский**.
- Код и идентификаторы — английский.
- **Источник неприкосновенен** — под `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`.**
## Команды
@@ -134,43 +64,140 @@ OpenSpec (пилот — `ingest`). До переноса источник ис
- `task run` — локальный запуск (`go run ./cmd/jellybit --config ./config.toml`)
- `task build` — статический бинарь `linux/amd64` для сервера
- `task test` / `task lint` — тесты и golangci-lint
- `task gate` — детерминированный гейт ревью (build/vet/lint/test/race/покрытие
изменённых строк/миграции/секреты); блокирует опиниативные проходы ревью
- `task gate` — детерминированный гейт ревью (см. ниже)
- `task review:context` — карта проекта для архитектурного прохода ревью
- `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`.
## Гейт
- **Команда целиком:** `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/<пакет>` по
компонентам из [architecture.md](docs/specs/architecture.md).
- Механизируемое проверяет `task gate` (`.golangci.yml` + `internal/archrules`):
форма ошибок и логов, конфиг мимо env, время мимо `store.Now()`, AUTOINCREMENT
в миграциях, направление зависимостей ядро↔транспорты. Пересказывать эти
правила не нужно — гейт скажет точнее.
- Прозой остаётся то, что правилом не выражается, и читается в источнике:
[ошибки](docs/conventions/errors.md) (трансляция доменной ошибки на внешней
границе, sentinel против типизированной),
[логи](docs/conventions/logging.md) (уровень по адресату, единственный
логирующий чокпоинт, `ext.*`, что не логируем),
[конфиг](docs/conventions/config.md) (секреты рендерит деплой в файл `0600`,
самодокументируемый `config.example.toml`, валидация на старте),
[БД](docs/conventions/database.md) (время в UTC RFC 3339, TEXT ULID через
`internal/ident`, `ident.Parse` на входной границе).
- Раскладка: `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/specs/database.md](docs/specs/database.md).
- Веб-UI на htmx — единый партиал = страница = фрагмент, ветвление по `isHTMX`,
деградация без JS, ошибка на htmx-пути = 200 + фрагмент, самозавершающийся
поллинг: [docs/conventions/web-ui.md](docs/conventions/web-ui.md).
нужен код): при изменении структуры в том же change обновляем ER-схему в
[docs/database.md](docs/database.md) — иначе краснеет шаг `canon` гейта.
Кросс-каттинг конвенции (как пишем код, а не что система делает) живут в
[docs/conventions/](docs/conventions/README.md) и не переносятся в OpenSpec.
Механизируемое там **не держим**: правило уезжает в `.golangci.yml` или в
`internal/archrules` и вычёркивается из прозы и из промптов ревью — процедура в
[references/promote.md](.claude/skills/review-pipeline/references/promote.md).
Прозой остаётся только то, что правилом не выражается.
## Язык
- Документация, комментарии, сообщения коммитов — **русский**.
- Код и идентификаторы — английский.