## 1. Фундамент: пакет ident - [x] 1.1 Добавить зависимость `github.com/oklog/ulid/v2`; пакет `internal/ident`: `NewID()` (lowercase, monotonic entropy, потокобезопасно), `NewIDAt(t time.Time)` (для миграции/бэкфилла), `Parse(s)` (нормализация регистра + валидация); тесты (lowercase, сортируемость, отказ на мусоре) ## 2. Схема и миграция - [x] 2.1 Механизм Go-миграций goose в `internal/store/migrations` (регистрация через `goose.AddMigrationContext`, совместный прогон с embed SQL-миграциями) - [x] 2.2 Миграция 0006: новые таблицы с TEXT ULID PK (все шесть), перенос данных с маппингом `int → ULID` (timestamp из `created_at`, fallback — время миграции), перенос `download.infohash` → `download_infohash` (lowercase, `kind` по длине hex), удаление `download.infohash` и `download.idempotency_key`, пересоздание индексов - [x] 2.3 Тест миграции на фикстурной БД: FK-связи сохранены, порядок по `id` = порядок по `created_at`, хеши разнесены, `idempotency_key` отсутствует ## 3. Store - [x] 3.1 Типы id `int64 → string` во всех структурах и методах `store` (download, recognition, hint, override, metadata_candidate, file_link, list); генерация ULID через `ident.NewID()` во ВСЕХ Create-методах (вместо `LastInsertId`) - [x] 3.2 Guarded-методы инварианта (design D4): `CreateDownloadIfNoActive` и `ActivateIfNoOtherActive`, каждый — одна write-транзакция; `FindActiveByInfohash`/`ExistsByInfohash` join'ом по любому хешу (для чтения); убрать CASE-восстановление `idempotency_key` из `SetDownloadState` - [x] 3.3 Методы хешей: добавить недостающие хеши загрузке (INSERT OR IGNORE), получать хеши вместе с Download (срез в модели) - [x] 3.4 Поиск в списке (`listWhere`): `EXISTS`-подзапрос по `download_infohash` вместо `LIKE` по удаляемому `download.infohash` ## 4. Ядро и воркер - [x] 4.1 Приём (`ingest`) и discover-adopt — через `CreateDownloadIfNoActive`; хеш из magnet (btih ИЛИ btmh, `kind` по длине hex) пишется в той же транзакции; сигнатуры `Result.DownloadID`, `notifyFailed`, `Notifier.Notify`, `failNotified` — на string - [x] 4.2 Поллинг/сверка: сопоставление раздачи по любому из хешей загрузки (Poll, desync, recovery, preflight); дописывание недостающих v1/v2, когда qBittorrent отдаёт оба; scoped-логгеры — первый известный хеш - [x] 4.3 Retry, recovery-воскрешение и Relink — через `ActivateIfNoOtherActive` (сейчас гонку закрывал unique-индекс — см. design D4); понятная ошибка при занятом хеше - [x] 4.4 `apply_batch_id` генерировать через `ident.NewID()` ## 5. Внешние границы - [x] 5.1 `httpapi`: `ident.Parse` в `pathID` (невалидный → 404 без похода в БД) и для `candidate_id` из формы ревью; сентинелы `downloadID > 0 → != ""`; REST DTO: `id` string, `infohash` → список `infohashes`; карточка показывает все хеши с копированием; live-лукап по любому хешу; проверить шаблоны и ссылки - [x] 5.2 Telegram (`tgbot`): `parseCallback` и callback-data на string-id, `pending map[int64]int64 → map[int64]string`, сентинел `id == 0 → == ""`, ссылки `/review/{id}`; понятный ответ на устаревшую callback-data со старым числовым id ## 6. Логи и документация - [x] 6.1 Атрибуты `_id` в логах: `recognition_id` у попыток распознавания, `batch_id` у раскладки; сверить с scoped-логгером - [x] 6.2 `docs/conventions/logging.md`: примеры id в формате ULID, grep по голому id как штатная корреляция - [x] 6.3 Новая `docs/conventions/database.md` (TEXT ULID PK, без AUTOINCREMENT, естественные ключи у деталей, lowercase + нормализация); ссылки из `docs/conventions/README.md` и `CLAUDE.md` - [x] 6.4 ER-схема `docs/specs/database.md`: ULID PK, `download_infohash`, удалённые столбцы ## 7. Проверка - [x] 7.1 `task test` и `task lint` зелёные; ручной прогон: приём magnet → дедуп повторного приёма → страница `/download/{id}` с ULID в URL