Правило, которое проверяет машина, не должно оставаться прозой: файл конвенций на сотни строк размазывает внимание по тривиальному — модель добросовестно проверит именование полей лога и не дойдёт до формы решения. Включены sloglint (константный msg, стиль ключ-значение), forbidigo (fmt.Print*, os.Getenv, time.Now мимо store.Now), errorlint (сравнение ошибок), depguard (сторонние пакеты ошибок). internal/archrules — сканеры на то, что линтером не выражается: направление зависимостей ядро↔транспорты, AUTOINCREMENT и серверное время в новых миграциях, матчинг ошибки по тексту. Код приведён к правилам: logging.StartCall как единая точка отсчёта длительности внешних вызовов, store.Now вместо time.Now в httpapi и часах воркера, slog.DiscardHandler в тестах. Перенесённое вычеркнуто из docs/conventions/* и openspec/config.yaml — прозой осталось только то, что правилом не выражается. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
134 lines
8.5 KiB
Markdown
134 lines
8.5 KiB
Markdown
# Конфигурация
|
||
|
||
Конвенция: *как* устроена и грузится конфигурация jellybit (TOML).
|
||
Правила оформления кода (How), не спецификация поведения.
|
||
|
||
Краткая выжимка и инварианты — в [CLAUDE.md](../../CLAUDE.md), раздел
|
||
«Конвенции кода».
|
||
|
||
> Каркас. Загрузчик `internal/config/config.go` уже грузит TOML; валидация
|
||
> на старте — в работе (`TODO`), обкатывается на следующем шаге.
|
||
|
||
## Принципы
|
||
|
||
- **Конфигурация — только TOML.** Env-переменные для конфига **не
|
||
используем**: окружение наследуется дочерними процессами и видно через
|
||
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
|
||
Запрет `os.Getenv` механизирован (`forbidigo`).
|
||
- Грузим **один раз при старте** в одну типизированную структуру `Config`
|
||
(под-структуры по секциям). Дальше по коду читаем только её — чтения файла в
|
||
бизнес-коде нет, только загрузчик `internal/config`.
|
||
- Конфиг **неизменяем** после старта; смена параметров — рестарт процесса.
|
||
|
||
## Файл и поиск
|
||
|
||
- Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочей
|
||
директории** процесса.
|
||
- Путь переопределяется опцией **`--config=path`**.
|
||
- Образец в репозитории — **`config.example.toml`** (см. ниже); реальный
|
||
`config.toml` не коммитится.
|
||
|
||
## config.example.toml — самодокументируемый образец
|
||
|
||
`config.example.toml` коммитим как единый справочник по конфигу: все секции
|
||
и все поля. **Каждое поле снабжаем комментарием**, из которого ясно:
|
||
|
||
- **зачем** поле — что оно меняет в поведении;
|
||
- **диапазон/допустимые значения** — перечисление или границы;
|
||
- **единицы измерения**, если применимо — секунды/миллисекунды, байты/КБ,
|
||
доля `0–1` и т.п.
|
||
|
||
```toml
|
||
[worker]
|
||
poll_interval = "5s" # как часто опрашивать qBittorrent; Go-duration (s/m/h)
|
||
magnet_timeout = "30m" # ждать метаданные magnet не дольше; Go-duration
|
||
source_missing_threshold = 3 # тиков сверки без раздачи, чтобы счесть источник удалённым
|
||
|
||
[recognition]
|
||
auto_confidence_threshold = 0.85 # порог авто-раскладки без ревью; доля 0.0–1.0
|
||
|
||
[llm]
|
||
max_retries = 3 # попыток получить валидный ответ LLM; целое ≥ 0
|
||
```
|
||
|
||
Секретные поля оставляем пустыми — значение приходит из деплоя (см.
|
||
«Секреты»).
|
||
|
||
## Поля по дискриминатору `type`
|
||
|
||
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного
|
||
из бекендов/внешних сервисов — напр. `[llm].type`), обязательность и
|
||
опциональность полей определяются значением `type`, а не фиксированы для
|
||
секции.
|
||
|
||
- **Валидация — по `type`.** Для каждого поддерживаемого `type` свой набор
|
||
обязательных полей; поля, относящиеся к другим `type`, не требуются.
|
||
Неизвестный `type` → ошибка на старте с перечислением поддерживаемых.
|
||
- **Образец — по `type`.** В `config.example.toml`:
|
||
- основной (дефолтный) `type` **предзаполнен** рабочими значениями;
|
||
- альтернативные `type` — **блоками-комментариями ниже**, каждый со своим
|
||
описанием полей (зачем/диапазон/единицы — как у обычных полей);
|
||
- так из примера видны все варианты и поля каждого, не открывая код.
|
||
|
||
```toml
|
||
[llm]
|
||
type = "openai-compat" # бекенд LLM; варианты ниже
|
||
base_url = "http://host.docker.internal:1234/v1" # эндпоинт OpenAI-совместимого API
|
||
api_key = "" # ключ; пусто для keyless-local (LM Studio)
|
||
model = "qwen2.5-32b-instruct" # имя модели у провайдера
|
||
|
||
# --- альтернативный бекенд: type = "<other>" ---
|
||
# [llm]
|
||
# type = "<other>" # описание варианта
|
||
# ... # его обязательные/опциональные поля
|
||
```
|
||
|
||
## Секреты
|
||
|
||
Секреты доставляет **деплой**, рендеря их прямо в `config.toml` (jellybit:
|
||
Ansible + Vault). Приложение просто читает TOML — отдельного слоя секретов
|
||
в коде нет. Источник истины секрета — внешнее хранилище деплоя (Vault), не
|
||
репозиторий и не env.
|
||
|
||
- Секретные поля jellybit: `qbittorrent.password`, `llm.api_key`,
|
||
`metadata.*.api_key`, `jellyfin.api_key`, `telegram.token`.
|
||
- Рендеренный `config.toml` (с секретами) **не коммитится**; права `0600`,
|
||
владелец — runtime-пользователь (`1000:1000`).
|
||
- В `config.example.toml` секретные поля — пустые строки.
|
||
- Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит
|
||
криво отрендеренный файл) — см. «Валидация и fail-fast».
|
||
- В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность».
|
||
|
||
## Валидация и fail-fast
|
||
|
||
Конфиг валидируем **на старте, до приёма трафика**. Невалидный конфиг —
|
||
лог `ERROR` и выход с ненулевым кодом (не стартуем «наполовину»).
|
||
|
||
Что проверяем (jellybit):
|
||
|
||
- обязательные поля заданы (напр. `qbittorrent.url`, `paths.*`,
|
||
`storage.db_path`);
|
||
- пути `paths.movies`/`series`/`downloads` существуют и доступны; целевые —
|
||
под единой песочницей (см. инварианты в [CLAUDE.md](../../CLAUDE.md));
|
||
- диапазоны: `recognition.auto_confidence_threshold` ∈ [0, 1],
|
||
`llm.max_retries` ≥ 0;
|
||
- длительности парсятся (`llm.timeout`, `worker.poll_interval`, …);
|
||
- `general.timezone` — распознаваемая IANA-зона (валидируется
|
||
`time.LoadLocation`; zoneinfo встроен через `time/tzdata`, поэтому ошибка =
|
||
битое имя, а не отсутствие базы в окружении);
|
||
- включённые секции консистентны: `metadata.tmdb.enabled` → задан `api_key`;
|
||
`jellyfin.enabled` → заданы `url`+`api_key`; `telegram.enabled` → `token`.
|
||
|
||
**Таймзоны.** Хранение времени в БД и логи — всегда UTC. Зона **отображения** в
|
||
веб-UI задаётся `[general].timezone` (дефолт `UTC`); только она конфигурируема,
|
||
на хранение/сортировку/логи не влияет. Бизнес-логика оперирует временем с явным
|
||
TZ (см. [CLAUDE.md](../../CLAUDE.md)).
|
||
|
||
## Структура в коде
|
||
|
||
- Весь разбор и валидация — в `internal/config`; наружу отдаётся готовая
|
||
`Config`.
|
||
- Одна корневая структура `Config` с под-структурами по секциям
|
||
(`QBittorrent`, `Paths`, `LLM`, `Metadata`, `Jellyfin`, `Worker`,
|
||
`Recognition`, `Telegram`, `HTTP`, `Log`).
|