ADR-0032: Viewable ID scheme (TR-XXXXXXX)

Status: accepted Date: 2026-04-21 Deciders: команда проекта Accepted-by: Phase 1-a ingestion loop landed; canonical_products.viewable_id UNIQUE populated by CanonicalProvisioner + ViewableIDIssuer in production path (see commits b5933de, a672448, 2ac96c0).

Контекст

Публичный API (/v1/products) возвращает клиенту идентификатор продукта. Внутренний canonical_products.id — UUIDv7 (ADR-0004). UUID плохо копируется, плохо диктуется голосом, плохо печатается на бумаге (путаница 0/O, 1/I/L). Для B2B-контекста (менеджер по телефону, склад с распечатанной спецификой) нужен короткий, произносимый, устойчивый к опечаткам ID помимо UUID.

Решение

Ввести колонку canonical_products.viewable_id TEXT UNIQUE с форматом:

TR-XXXXXXX
  • TR- — фиксированный ASCII-префикс (от “Tracium”).
  • XXXXXXX — 7 символов Crockford base32 (алфавит 0-9A-HJKMNP-TV-Z, без I/L/O/U).
  • Регекс валидации: ^TR-[0-9A-HJKMNP-TV-Z]{7}$.

Генерация: crypto/rand → 5 raw bytes → base32 encode (custom alphabet, no padding) → first 7 chars → prefix. При DB-коллизии retry до 3 раз; исчерпание → ErrCollisionsExhausted (deg. UniqueChecker signal).

Пространство: 32⁷ ≈ 34·10⁹. При 10⁵ записей p(коллизии на запись) ≈ 1.5·10⁻⁵; 3 retry снижают до ~10⁻¹⁵.

Стабильность: viewable_id неизменяем после create. При canonical merge в Phase 2 оба ID сохраняются через canonical_aliases (alias_id → survivor_id); запрос по любому ID → survivor.

Последствия

Плюсы

  • Короткий copy-paste friendly ID для клиентов.
  • Crockford избавляет от глазных опечаток (0/O, 1/I/L).
  • Неперебираемый (crypto/rand) — защищает от scraping.
  • Реализация изолирована в platform/viewableid; потребители зовут через UniqueChecker port без прямой связи с DB из generator’а.

Минусы

  • Дополнительная колонка + UNIQUE индекс на canonical_products (~8 байт + btree overhead).
  • Retry на DB-коллизии добавляет round-trip при редких (<10⁻⁵) collision events.

Нейтральные

  • Для URL-friendly, но не SEO-friendly (нет слов). URL типа /products/TR-8F2K3P2 — нормально для B2B.

Рассмотренные альтернативы

A. Использовать UUID напрямую в URL

Отклонено: ADR-0004 делает canonical_products.id = UUIDv7, 36 символов, плохо диктуется.

B. Sequence + base36

Отклонено: TR-12345 легко перебрать — scraper пройдёт по каталогу.

C. HMAC от sequence

Отклонено: добавляет secret-management, rotation complexity; детерминированная обратимость не даёт продуктовой ценности.

D. nanoid / cuid

Отклонено: включает lowercase + ambiguous oO0, требует pre-process перед голосом/печатью.

Ссылки

Outcomes (Phase 1-a)

  • platform/viewableid exports Generator with crypto/rand + retry 3× — coverage 92.6%, ErrCollisionsExhausted sentinel.
  • canonical_products.viewable_id TEXT UNIQUE (migration 0009) + GIN-free plain btree index.
  • canonical_aliases(alias_id PK → survivor_id) ready for Phase 2 merge flow; iter-1 untouched.
  • Public GET /v1/products/{id_or_viewable} handler reserves the route but returns 404 in iter-1 — real lookup Phase 2.