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/дорожной карте (нагрузка и связность), не пересматривалось.