ADR-0011 · Ладдерные данные живут в play-БД; analytics — source-agnostic корпус

Статус: принято · 2026-07-16 (план: 08 Лэддер и webhook). Пересматривает часть ADR-0009/плана 08 о месте расчёта Glicko-2.

Контекст. Analytics с самого начала строилась как независимый мультиисточниковый агрегатор (dicechess.com, beturanga, playsite, будущие источники) с декларативно source-agnostic схемой («Writers map their source’s vocabulary onto these canonical values») и публичным read API (его читает dicechess-analytics-ui). Исходный план 08 предполагал: B.3 расширяет ingest-контракт полями server_seed/client_seeds/pairing_id, а Glicko-2 (D.1) и SPRT/WHR (E.1) считаются в analytics. К моменту решения фазы B/C фактически создали «специфическую БД play-сайта»: game_results (play-api#98) несёт rated, результат, участников, server_seed, pairing_id с готовыми запросами под рейтинг-батч и CRN-анализ, а рейтинг-состояние bots.glicko_* (play-api#100) и leaderboard API (D.2, play-api#103) и так живут на стороне play.

Решение. Play-БД — system of record для всего ладдерного и playsite-специфичного (seed’ы, pairing, рейтинг-состояние, game_results). Ingest-контракт не расширяется: analytics продолжает получать партии в сегодняшнем виде — без seed/pairing — и остаётся обезличенным корпусом для позиционной аналитики, opening book и training-экспортов. Следствия по задачам: B.3 (play-api#99) отменена; D.1 переезжает в play-api (#119 — Glicko-2 батч по finishedRatedSince, фоновый fiber по образцу LadderScheduler, пишет bots.glicko_* в той же БД); E.1 переезжает в play-api (#120 — SPRT/WHR report-runner по pairing_id из game_results); A.1 (analytics#245) не затронута — она анализирует корпус, уже находящийся в analytics.

Последствия.

  • Три причины-драйвера: (1) read API analytics публичный — отправка общего сида CRN-пары в момент завершения первой партии (партнёр ещё играет) воспроизвела бы утечку reveal-timing (play-api#115) на стороне analytics, затащив знание о жизненном цикле пар в source-agnostic сервис; (2) выделенные колонки под provably-fair механику одного источника из трёх противоречат дизайну схемы analytics (для специфики там есть metadata_json); (3) в исходном плане не был спроектирован обратный поток analytics→play для лидерборда — расчёт дома замыкает цикл D.1→D.2 внутри одной БД.
  • Самый рискованный кросс-репо шаг плана (verbatim-контракт, три зеркала, 422-radius) исчезает; спасённая часть B.3 — фикс уже существовавшего дрейфа types.ts в SPA — вынесена отдельно (play#113).
  • play-api получает вычислительную обязанность (рейтинг-батч) — вне игрового write-path, так что принцип «ноль нагрузки на игровой поток» из 01-Dorozhnaya-karta сохраняется; на текущем масштабе стоимость тривиальна, индекс (rated, finished_at) строился под это.
  • game_results сознательно без FK на games — переживает будущую чистку тяжёлых JSONB-снапшотов, рейтинговая история не теряется.
  • Если позже понадобится отдавать seed/pairing в analytics (корпусный аудит честности костей) — путь через одно generic-поле metadata в ingest (в games.metadata_json), не через специфичные колонки. Отложено как YAGNI.

Альтернативы (отклонены). Исходный B.3-план (расширить контракт, считать в analytics) — три согласованных PR с продовым 422-риском, sекретный seed за публичным API, и всё равно нужен несуществующий канал analytics→play для рейтингов. Отдельная новая БД для лэддера — третье хранилище без выгод: game_results уже даёт изоляцию от JSONB-снапшотов внутри существующей play-БД. Считать рейтинг живьём в игровом сервисе — отклонено ещё в 07/дорожной карте (нагрузка и связность), не пересматривалось.

🔗 08-Reyting-botov-i-webhook-plan-vypolneniya · ADR-0009-Bot-API-i-turniry · 06-Chestnost-kubikov-provabli-feyr-i-verifikatsiya · 00-Zhurnal-resheniy-ADR