## Why Идентичность в домене сегодня держится на двух хрупких вещах: загрузка фактически идентифицируется инфохэшем (`idempotency_key`), хотя у одной логической загрузки хешей несколько (v1/v2/гибрид, перезалив — другой хеш), а первичные ключи всех таблиц — числовые автоинкременты, не уникальные между таблицами и неудобные для корреляции в логах. Это фундамент (шаг 1 черновика [logical-title-model](../../../docs/drafts/logical-title-model.md)) для «второго сезона», «докачивания» и истории переходов; менять PK дешевле сейчас, пока БД маленькая и ссылок на идентификатор немного. ## What Changes - **ULID (канонически lowercase) как TEXT PK всех сущностей**: `download`, `recognition`, `hint`, `override`, `metadata_candidate`, `file_link`. Генерация — в приложении (`oklog/ulid`, monotonic entropy). **BREAKING**: формат id меняется в URL (`/download/{id}`, `/review/{id}`), ссылках и callback-data Telegram-бота, логах; в REST JSON поле `id` меняет тип `number → string`, поле `infohash` заменяется списком `infohashes`. - **Новая таблица `download_infohash`** (`download_id`, `infohash`, `kind` v1|v2, составной PK `(infohash, download_id)` — один хеш легитимно принадлежит нескольким загрузкам во времени) — множество хешей одной загрузки; дедуп переезжает на проверку активности по этой таблице, столбцы `download.idempotency_key` и `download.infohash` удаляются. - **Поиск по любому из хешей** — при приёме (дедуп) и в поллинге qBittorrent. - `apply_batch_id` генерируется как ULID (столбец уже TEXT). - **Go-миграция goose**: backfill ULID существующим строкам с timestamp-частью из `created_at` (сортировка id сохраняет хронологию), переписывание FK, разнос текущего `infohash` в `download_infohash`. - **Конвенция `docs/conventions/database.md`**: PK — TEXT ULID, генерится приложением; числовой AUTOINCREMENT не используем; у деталей/связей допустим естественный ключ. - **Логи**: у каждой сущности поле `_id`; глобальная уникальность ULID делает grep по голому id штатным способом корреляции; обновить примеры в `docs/conventions/logging.md`. - ER-схема `docs/specs/database.md` обновляется в этом же change. ## Capabilities ### New Capabilities - `identity`: как система идентифицирует сущности домена — ULID-ключи и их канонический вид (нормализация на входных границах), множество инфохэшей загрузки, инвариант «одна активная загрузка на infohash» (дедуп при приёме), сопоставление раздачи в поллинге по любому из хешей. ### Modified Capabilities - `state-reconciliation`: требование «терминализация восстанавливает `idempotency_key`» меняется — инвариант «одна активная задача на infohash» обеспечивается проверкой активности по `download_infohash`, отдельный снимаемый/восстанавливаемый ключ исчезает. ## Impact - **Код**: `store` (типы id `int64 → string`, все запросы, миграция), `ingest` (дедуп через `download_infohash`), `worker` (сопоставление в поллинге по множеству хешей), `httpapi`/веб-UI (парсинг и валидация ULID в `/download/{id}`, ссылки), Telegram-уведомления (ссылки на загрузку). - **Зависимости**: + `github.com/oklog/ulid/v2` (чистый Go, CGO не нужен). - **БД**: пересоздание всех шести таблиц (SQLite меняет PK только через rebuild) одной миграцией; первая Go-миграция в проекте — goose до сих пор использовался только с SQL-файлами, нужна регистрация Go-миграций. - **Документация**: новая `docs/conventions/database.md`, правки `docs/conventions/logging.md`, ER-схема `docs/specs/database.md`, ссылка на новую конвенцию из `CLAUDE.md`/README конвенций. - **Не меняется**: семантика дедупа (нашли активную загрузку по любому хешу → та же загрузка), явные `ORDER BY created_at` в списках, инварианты безопасности данных.