# identity Specification ## Purpose Как система идентифицирует сущности домена: ULID-ключи (канонический lowercase-вид, нормализация и валидация на входных границах) и корреляция сущностей в логах по id. Инфохэши загрузки (`download_infohash`), дедупликация приёма и инвариант «не более одной активной загрузки на infohash» (атомарный возврат в активное состояние) — в capability `ingest`. ## Requirements ### Requirement: ULID как первичный ключ сущностей Каждая сущность домена SHALL иметь первичный ключ ULID — TEXT, 26 символов Crockford base32, генерируемый приложением в момент создания записи через единственную точку генерации (`internal/ident`). Сущности: `download`, `recognition`, `hint`, `override`, `metadata_candidate`, `file_link`. Канонический вид SHALL быть lowercase. Числовые AUTOINCREMENT-ключи в новых таблицах использоваться SHALL NOT. Идентификатор партии раскладки (`apply_batch_id`) SHALL генерироваться тем же способом. #### Scenario: Создание загрузки - **WHEN** принимается новая загрузка - **THEN** её `id` — валидный ULID в lowercase - **AND** `id` уникален глобально (не совпадает с id других сущностей) #### Scenario: Хронологическая сортировка - **GIVEN** две загрузки, созданные последовательно - **WHEN** записи сортируются по `id` лексикографически - **THEN** порядок совпадает с порядком создания ### Requirement: Нормализация и валидация id на входных границах Внешние идентификаторы SHALL валидироваться как ULID и нормализоваться к lowercase до обращения к хранилищу — это касается всех входных границ: URL `/download/{id}`, параметры форм и команд. Синтаксически невалидный id SHALL обрабатываться как несуществующая сущность (404 для страниц), без обращения к БД. #### Scenario: Uppercase-вариант id в URL - **GIVEN** существующая загрузка с id `01jz…` (lowercase) - **WHEN** клиент открывает `/download/01JZ…` (uppercase) - **THEN** открывается страница той же загрузки #### Scenario: Мусор вместо id - **WHEN** клиент открывает `/download/abc!!!` - **THEN** ответ — 404, запрос к БД не выполняется ### Requirement: Корреляция сущностей в логах Записи журнала, относящиеся к сущности, SHALL содержать её id в атрибуте `_id` (`download_id`, `recognition_id`, `batch_id`, …); работа в контексте загрузки ведётся через scoped-логгер с `download_id`. Благодаря глобальной уникальности ULID поиск по значению id (grep/jq) SHALL находить все записи журнала, относящиеся к сущности, независимо от имени поля. #### Scenario: Путь загрузки по логам - **GIVEN** загрузка прошла приём, распознавание и раскладку - **WHEN** журнал фильтруется по значению её `id` - **THEN** находятся записи всех этапов (ingest, recognition, file-layout) ### Requirement: Миграция существующих записей Существующие записи SHALL получить ULID-идентификаторы одной миграцией с сохранением всех связей (FK) и хронологии: timestamp-часть ULID SHALL браться из `created_at` записи, чтобы лексикографический порядок новых id соответствовал историческому порядку создания. Существующий `download.infohash` SHALL быть перенесён в `download_infohash` (нормализация к lowercase, `kind` по длине hex: 40 — `v1`, 64 — `v2`); столбцы `download.infohash` и `download.idempotency_key` SHALL быть удалены. #### Scenario: Связи и порядок после миграции - **GIVEN** БД с загрузками, распознаваниями и файловыми ссылками на числовых id - **WHEN** миграция выполнена - **THEN** все FK-связи сохранены (распознавания/ссылки указывают на те же загрузки) - **AND** порядок загрузок по `id` совпадает с порядком по `created_at` - **AND** каждый прежний `infohash` представлен записью в `download_infohash`