Files
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

82 lines
5.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 Атрибуты `<entity>_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