Raw-архив — bronze-слой сырых данных

Решение (2026-06-19): ввести durable immutable raw-архив сырых JSON dicechess.com перед нормализацией. При найденных багах конвертации → переконвертировать архив, а не перескрейпить Cloudflare-gated сайт.

Зачем (выгоды конкретны, не гипотетичны)

  1. Защита от перескрейпа. Сайт за Cloudflare (проходит только curl + OpenSSL ≥ 3.5), single-flight ~1.5–2 партии/сек. game-move-history — point-in-time снимок: при 422 сырьё уже не перезапросить. Сохранённое сырьё — единственный способ сделать 422 и баги парсера восстановимыми.
  2. Реплей при багах конвертации — главная выгода. Нормализатор — хрупкие эвристики: en passant #351, инференс цвета по часам, double-decline vs draw, terminal-color (#161/#163). Каждый баг = правка кода, требующая переконвертации уже скачанных партий.
  3. Развязка добычи и конвертации. Медленный/сломанный конвертер не стопорит добычу; падение конвертера не теряет партию, прошедшую Cloudflare.

Ключевой факт: это НЕ новая система

Это формализация уже построенного в dicechess-sync (см. dicechess-sync). Там стадия cache(raw) уже есть: таблица raw_game_data (SQLite, move_history_json), fetchGame()→markFetched() без нормализации, postGame() читает кэш → нормализует → POST, есть replayRejected()/replayNormalizeFailures().

Что доделать:

  • Поднять raw_game_data из «transient cache» в архив-источник-истины: gzip BYTEA + provenance (source, content_hash, schema_version, отдельный meta_fetched_at), retention = хранить вечно.
  • Запись = bundle {discovery_meta + move_history} (два артефакта, приходят в разное время), PK (source, external_id)НЕ id один (dicechess UUID vs beturanga ObjectId коллизируют).
  • Хранить gzip BYTEA, не JSONB (×20 по месту, байт-в-байт для хэша/реплея).
  • Сырьё реплеим, а не запрашиваем по полям → NoSQL не нужен.

Решения по границам

  • В публичный dicechess-analytics raw НЕ добавляем (он source-agnostic; metadata_json = NULL). Архив живёт только на приватной стороне.
  • Запрос «по игроку» = вторичный индекс над неизменяемыми партиями. player/history — растущий изменяемый снимок, НЕ immutable → ключ архива = ПАРТИЯ, страницу истории хранить лишь как discovery_meta в bundle.

Главная дыра сейчас

observer (rpi4) и extension — memory-only, теряют сырьё live-партий безвозвратно. Самый ценный конкретный шаг — заставить observer писать bundle в архив до normalizeStateMap().

Масштаб и размещение

~2.4 КБ/партия gzipped (сырьё ~48 КБ, сжатие ~20×). 10M ≈ 24 ГБ — не big-data. → Архив на dexus (192.168.10.4): 825 ГБ свободного HDD, уже крутит borgbackup. HDD идеален для append-only write-once-read-rarely. SQLite на single-writer достаточно; PG — если несколько писателей.

Топология

[curl fetch] → [RAW ARCHIVE (dexus)] → [converter читает архив] → [analytics POST]. Не два отдельных микросервиса — один процесс на источник, но «запись сырого» = жёсткая точка коммита перед нормализацией. Реплей после фикса движка = перезапуск converter по архиву.

Связано: dicechess-sync, Dice Chess Observer, Апгрейд aurora — план и чек-лист.