Files
jellybit/openspec/changes/archive/2026-07-02-ulid-identity/tasks.md
T
avandClaude Fable 5 37f2f6481a Идентичность на ULID: download_infohash, guarded-дедуп, миграция (ulid-identity)
Все сущности переехали с INTEGER AUTOINCREMENT на TEXT ULID (lowercase,
internal/ident — единая точка генерации и разбора; oklog/ulid). Инфохэши
загрузки — множество (download_infohash, v1/v2 гибридных торрентов): дедуп
и сопоставление в поллинге по любому из хешей, magnet-парсер отдаёт оба
хеша гибридной ссылки, усечённый v2-хеш v2-only раздач не хранится.

Инвариант «не более одной активной загрузки на infohash» вместо снятого
unique-индекса держат guarded-методы store в одной write-транзакции
(_txlock=immediate): CreateDownloadIfNoActive (приём/adopt, с доносом
недостающих хешей), ActivateIfNoOtherActive (retry/recovery/relink, отказ
до побочных эффектов), guarded AddInfohashes; SetDownloadState отклоняет
терминал→активное как механический бэкстоп.

Миграция 0006 — первая Go-миграция goose: пересоздание таблиц при
включённых FK, backfill ULID с timestamp из created_at (хронология id
сохранена), разнос infohash, удаление idempotency_key. BREAKING: формат id
в URL/логах/Telegram, REST-поля id (string) и infohashes (список).

Новая конвенция docs/conventions/database.md (без числовых PK), корреляция
в логах grep'ом по голому ULID, ER-схема обновлена. Спеки: новая capability
identity, MODIFIED в state-reconciliation; change заархивирован. Пройдены
ревью дизайна и кода (по 8 углов), все находки исправлены с
регрессионными тестами.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:25:00 +03:00

5.3 KiB
Raw Blame History

1. Фундамент: пакет ident

  • 1.1 Добавить зависимость github.com/oklog/ulid/v2; пакет internal/ident: NewID() (lowercase, monotonic entropy, потокобезопасно), NewIDAt(t time.Time) (для миграции/бэкфилла), Parse(s) (нормализация регистра + валидация); тесты (lowercase, сортируемость, отказ на мусоре)

2. Схема и миграция

  • 2.1 Механизм Go-миграций goose в internal/store/migrations (регистрация через goose.AddMigrationContext, совместный прогон с embed SQL-миграциями)
  • 2.2 Миграция 0006: новые таблицы с TEXT ULID PK (все шесть), перенос данных с маппингом int → ULID (timestamp из created_at, fallback — время миграции), перенос download.infohashdownload_infohash (lowercase, kind по длине hex), удаление download.infohash и download.idempotency_key, пересоздание индексов
  • 2.3 Тест миграции на фикстурной БД: FK-связи сохранены, порядок по id = порядок по created_at, хеши разнесены, idempotency_key отсутствует

3. Store

  • 3.1 Типы id int64 → string во всех структурах и методах store (download, recognition, hint, override, metadata_candidate, file_link, list); генерация ULID через ident.NewID() во ВСЕХ Create-методах (вместо LastInsertId)
  • 3.2 Guarded-методы инварианта (design D4): CreateDownloadIfNoActive и ActivateIfNoOtherActive, каждый — одна write-транзакция; FindActiveByInfohash/ExistsByInfohash join'ом по любому хешу (для чтения); убрать CASE-восстановление idempotency_key из SetDownloadState
  • 3.3 Методы хешей: добавить недостающие хеши загрузке (INSERT OR IGNORE), получать хеши вместе с Download (срез в модели)
  • 3.4 Поиск в списке (listWhere): EXISTS-подзапрос по download_infohash вместо LIKE по удаляемому download.infohash

4. Ядро и воркер

  • 4.1 Приём (ingest) и discover-adopt — через CreateDownloadIfNoActive; хеш из magnet (btih ИЛИ btmh, kind по длине hex) пишется в той же транзакции; сигнатуры Result.DownloadID, notifyFailed, Notifier.Notify, failNotified — на string
  • 4.2 Поллинг/сверка: сопоставление раздачи по любому из хешей загрузки (Poll, desync, recovery, preflight); дописывание недостающих v1/v2, когда qBittorrent отдаёт оба; scoped-логгеры — первый известный хеш
  • 4.3 Retry, recovery-воскрешение и Relink — через ActivateIfNoOtherActive (сейчас гонку закрывал unique-индекс — см. design D4); понятная ошибка при занятом хеше
  • 4.4 apply_batch_id генерировать через ident.NewID()

5. Внешние границы

  • 5.1 httpapi: ident.Parse в pathID (невалидный → 404 без похода в БД) и для candidate_id из формы ревью; сентинелы downloadID > 0 → != ""; REST DTO: id string, infohash → список infohashes; карточка показывает все хеши с копированием; live-лукап по любому хешу; проверить шаблоны и ссылки
  • 5.2 Telegram (tgbot): parseCallback и callback-data на string-id, pending map[int64]int64 → map[int64]string, сентинел id == 0 → == "", ссылки /review/{id}; понятный ответ на устаревшую callback-data со старым числовым id

6. Логи и документация

  • 6.1 Атрибуты <entity>_id в логах: recognition_id у попыток распознавания, batch_id у раскладки; сверить с scoped-логгером
  • 6.2 docs/conventions/logging.md: примеры id в формате ULID, grep по голому id как штатная корреляция
  • 6.3 Новая docs/conventions/database.md (TEXT ULID PK, без AUTOINCREMENT, естественные ключи у деталей, lowercase + нормализация); ссылки из docs/conventions/README.md и CLAUDE.md
  • 6.4 ER-схема docs/specs/database.md: ULID PK, download_infohash, удалённые столбцы

7. Проверка

  • 7.1 task test и task lint зелёные; ручной прогон: приём magnet → дедуп повторного приёма → страница /download/{id} с ULID в URL