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
+169
View File
@@ -0,0 +1,169 @@
# Архитектура
Обзор: как сложено и где что работает. **Поведение системы здесь не
описывается** — его нормативный дом `openspec/specs/`; ниже компоненты только
ссылаются на свои capability. Инварианты и их severity — в
[CLAUDE.md](../CLAUDE.md), схема хранилища — в [database.md](database.md),
периметр — в [security.md](security.md).
## Принципы
- **Один статический бинарь.** Доставка — образом с готовым бинарём внутри. См.
[ADR-2026-06-13-go-single-binary](adr/ADR-2026-06-13-go-single-binary.md).
- **Источник неприкосновенен.** Только `mkdir`, `link(2)` и `unlink` *своих*
целевых ссылок. См. [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md).
- **Выход распознавания недоверенный.** Безопасность держится на валидации
целевого пути, а не на промпте — [security.md](security.md).
- **Единое ядро, тонкие транспорты.** Логика приёма — в use-case `ingest`;
переходами состояний владеет `worker`. HTTP API, веб-UI, Telegram и CLI лишь
складывают команды, `worker` их сериализует.
- **Опциональные внешние зависимости.** Метабазы (TMDB/TVDB/TVMaze) и триггер
Jellyfin включаются конфигом; без них сервис работает, но авто-раскладка без
подтверждённого матча не делается —
[ADR-2026-06-13-auto-link-requires-db-match](adr/ADR-2026-06-13-auto-link-requires-db-match.md).
- **Минимум компонентов.** В духе umbar — без зоопарка сервисов.
## Компоненты
`cmd/jellybit` — точка входа и сборка зависимостей; всё остальное — `internal/*`.
| Пакет | Ответственность | Capability |
| --- | --- | --- |
| `ingest` | use-case приёма загрузки, общий для всех транспортов | [ingest](../openspec/specs/ingest/spec.md) |
| `magnet`, `torrent` | разбор magnet-ссылки и байтов `.torrent`, извлечение инфохэшей | [ingest](../openspec/specs/ingest/spec.md) |
| `worker` | владелец машины состояний: поллинг qBittorrent, сериализация команд, фоновая сверка | [download-tracking](../openspec/specs/download-tracking/spec.md), [state-reconciliation](../openspec/specs/state-reconciliation/spec.md), [live-status](../openspec/specs/live-status/spec.md) |
| `qbt` | клиент qBittorrent WebUI API (сессия, добавление, опрос, удаление) | [download-tracking](../openspec/specs/download-tracking/spec.md) |
| `recognize` | пред-парс имени, вызов LLM, разбор плана, модель уверенности | [recognition](../openspec/specs/recognition/spec.md) |
| `llm` | провайдер LLM за интерфейсом (дискриминатор `[llm].type`) | [recognition](../openspec/specs/recognition/spec.md) |
| `metadata` | интерфейс метабаз + TMDB/TVDB/TVMaze (опц.) | [metadata-match](../openspec/specs/metadata-match/spec.md) |
| `naming` | единая логика целевых имён и отображаемого имени раздачи | [file-layout](../openspec/specs/file-layout/spec.md), [ingest](../openspec/specs/ingest/spec.md) |
| `layout` | санитизация путей, хардлинкер, copy-fallback, undo, владение путём | [file-layout](../openspec/specs/file-layout/spec.md), [state-reconciliation](../openspec/specs/state-reconciliation/spec.md) |
| `store` | SQLite: загрузки, распознавания, подсказки, кандидаты, ссылки | [identity](../openspec/specs/identity/spec.md) |
| `ident` | генерация и нормализация ULID | [identity](../openspec/specs/identity/spec.md) |
| `httpapi` | REST + веб-UI на htmx (server-rendered партиалы) | [web-ui](../openspec/specs/web-ui/spec.md), [review](../openspec/specs/review/spec.md) |
| `tgbot` | Telegram: приём, парсер сообщений торрент-бота, карточки, пинги | [notifications](../openspec/specs/notifications/spec.md), [review](../openspec/specs/review/spec.md) |
| `jellyfin` | триггер пересканирования медиатеки (опц.) | [file-layout](../openspec/specs/file-layout/spec.md) |
| `config` | загрузка и валидация TOML на старте | — |
| `logging`, `logctx` | slog-настройка и протяжка корреляции через контекст | [identity](../openspec/specs/identity/spec.md) |
| `archrules` | собственный анализатор архитектурных правил (часть гейта) | — |
Транспорты приёма — HTTP API, веб-UI, Telegram и CLI (`jellybit add`) — ведут в
один `ingest`; действия пользователя (apply / refine / reject / defer / undo /
retry / delete / dismiss) идут командами к `worker`.
## Внешние границы и форматы
- **qBittorrent WebUI API** — единственный способ качать: источник (magnet, URL,
`.torrent`) **отдаём ему**, сами по пользовательскому URL не ходим (SSRF
исключён). Пути берём из API (`save_path` + относительные имена из
`/torrents/files`), не из константы.
- **LLM** — OpenAI-совместимый Chat Completions (`[llm].type = "openai-compat"`),
структурированный вывод через `response_format: json_object`; валидация ответа
своя, в Go.
- **Метабазы** — TMDB, TVDB, TVMaze (последняя без ключа, только сериалы).
- **Jellyfin** — один вызов `POST /Library/Refresh`, авторизация заголовком
`X-Emby-Token`.
- **Telegram Bot API** — приём сообщений и исходящие карточки/пинги.
- **Сообщение торрент-бота** — чужой текстовый формат, разбирается парсером
`tgbot`; наблюдения по формату — в
[research/torrent-bot-message.md](research/torrent-bot-message.md).
## Эксплуатация
- **Где работает, что рядом, кто перезапускает:** домашний медиа-сервер umbar
(Intel N150), docker в общей сети с qBittorrent и Jellyfin. Перезапускает
docker по `restart`-политике и плейбук umbar при редеплое; человек — руками,
когда всё плохо. Оператор один и он же владелец.
- **Внешние зависимости поимённо и чем каждая отказывает:**
| Зависимость | Обязательна | Как отказывает |
| --- | --- | --- |
| qBittorrent | да | недоступен (весь цикл встаёт, тик поллинга краснеет); отдаёт раздачу без файлов; теряет раздачу (пропажа источника); переходные состояния `moving`/`checking*` выглядят как готовность |
| LLM-эндпоинт | да | недоступен; отвечает медленно (минуты); отдаёт не-JSON или JSON не по схеме; отдаёт правдоподобную выдумку — самый неприятный случай, потому что молчаливый |
| TMDB/TVDB/TVMaze | нет | недоступны; лимит запросов; пустой результат (норма для русского контента); несколько равнозначных кандидатов |
| Jellyfin | нет | недоступен — скан просто не случится, состояние задачи не страдает |
| Telegram Bot API | нет | недоступен — уведомление теряется, состояние задачи не страдает |
| Диск `/srv/media` | да | переполнен (особенно на copy-fallback); ФС без хардлинков; файл исчез между проверкой и `link(2)` |
| SQLite | да | `database is locked` при конкурентной записи; файл тома не смонтирован |
- **Кто заметит отказ и когда:** владелец — по отсутствию ожидаемого пинга и по
задаче, застрявшей в промежуточном состоянии; логи в stdout контейнера.
Автоматического алертинга нет, метрик нет — только уведомления в Telegram о
падении загрузки и о рассинхроне.
- **Характер потока:** непрерывный фон (тик поллинга qBittorrent, по умолчанию
5 с, и периодическая сверка) плюс редкие события по запросу человека. Объём —
единицы загрузок в день, десятки одновременно; ориентир масштаба и его аудит —
задача в беклоге.
## Единые точки проекта
Материал для вопроса «не появился ли второй способ делать то, что уже делается».
| Что | Где |
| --- | --- |
| Время | `store.Now()` — единственный источник, всегда UTC; формат хранения — RFC 3339 |
| Идентификаторы | `internal/ident` — генерация и нормализация ULID; `ident.Parse` на каждой входной границе |
| Целевые имена и превью раскладки | `internal/naming` — одна логика для превью в UI и для реального применения |
| Разбор источника | `internal/magnet` и `internal/torrent`; инфохэш извлекается только здесь |
| Приём | use-case `ingest` — общий путь для HTTP, веб-UI, Telegram и CLI |
| Переходы состояний | `worker` под per-download блокировкой; легальность перехода задаётся декларативным графом |
| Хардлинки и удаление своих ссылок | `internal/layout` — единственное место, которое пишет в файловую систему библиотеки |
| Трансляция доменной ошибки в код ответа | внешняя граница транспорта (`httpapi`, `tgbot`); правило — [conventions/errors.md](conventions/errors.md) |
| Логирующий чекпоинт | доменная граница, один на операцию; правило — [conventions/logging.md](conventions/logging.md) |
| Настройки | один TOML-файл, валидируется на старте; образец `config.example.toml` — источник истины по полям |
## Деплой
Работает в docker в одной среде с qBittorrent и Jellyfin — см.
[ADR-2026-07-24-local-image-build](adr/ADR-2026-07-24-local-image-build.md).
Сборка: статический бинарь (`GOOS=linux GOARCH=amd64 CGO_ENABLED=0`) и **полный
образ** собираются локально на control-хосте (`task image` упаковывает бинарь в
`distroless/static`). Образ едет на сервер через `docker save`/`load` (роль
`app_image` в umbar), там и запускается. Go-тулчейн и `docker build` на сервере
не нужны.
Разделение ответственности: **jellybit** (этот репозиторий) даёт бинарь и
`Dockerfile`; **umbar** — оркестрацию (доставка, docker compose,
`playbook-jellybit.yml`, рендер секретов).
Параметры запуска:
- **Общая docker-сеть** (external, напр. `media-net`) — адресация по именам
(`http://qbit:8989`, `http://jellyfin:8096`). Веб-UI публикуется на хост
(`8080:8080`) для LAN. qBit валидирует Host-заголовок — в umbar выставлен
`WebUI\ServerDomains=*`; LLM на хосте достаётся через `host.docker.internal`.
- **`user: "1000:1000"`**, UMASK 022 — единый системный пользователь umbar.
- **mount `/srv/media`** — единая песочница (см. ниже).
- **mount конфига** `/srv/applications/jellybit/config``/config` (ro),
`config.toml` с правами `0600`; рендерится плейбуком umbar, бекапу не подлежит.
- **mount данных** `/srv/applications/jellybit/data``/data`, SQLite
`/data/jellybit.db`. **Бекапить обязательно** — без него редеплой стирает всё
in-flight состояние.
- **healthcheck** зовёт сам бинарь (`jellybit healthcheck`): в distroless нет
shell и curl.
### Единая песочница `/srv/media`
Весь медиа-стек лежит под одним каталогом и монтируется **идентично**
(`/srv/media:/srv/media`) во все медиа-приложения:
```
/srv/media/
incomplete/ ← qBit качает сюда
downloads/ ← готовые раздачи (источник хардлинка)
movies/ series/ ← библиотека Jellyfin (цель хардлинка)
```
Так как всё под одним mount'ом, работают и **хардлинк** (downloads →
movies/series), и **мгновенный move** qBit (incomplete → downloads) — границ
между точками монтирования (`EXDEV`) нет. Путь из qBittorrent уже равен
хост-пути, трансляция не нужна (`path_map` — фолбэк, обычно пуст). Секреты и
чужие приложения (`/srv/applications`) в песочницу не попадают. Библиотеки
Jellyfin указывают на `movies`/`series`, а не на корень — иначе в индекс попадут
`downloads`/`incomplete`.
## Открытые вопросы
- Пока нет. Решённое разъехалось по ADR и capability-спекам; то, что требует
работы, живёт задачами в [tasks/BACKLOG.md](tasks/BACKLOG.md).