Skip to content

docs(gamestate): win_prob/momentum fields + WS-vs-SSE delivery semantics#278

Merged
Mlaz-code merged 4 commits into
mainfrom
docs/gamestate-winprob-streaming-semantics
Jul 3, 2026
Merged

docs(gamestate): win_prob/momentum fields + WS-vs-SSE delivery semantics#278
Mlaz-code merged 4 commits into
mainfrom
docs/gamestate-winprob-streaming-semantics

Conversation

@Mlaz-code

Copy link
Copy Markdown
Collaborator

What

Documents two gamestate API surfaces that existed in production but were undocumented or documented wrong:

  1. Tennis win-probability fields (content/en/api-reference/gamestate.mdx) — new "Win probability (tennis)" section covering win_prob_home / win_prob_away / win_prob_book / win_prob_at, momentum / momentum_window_s, the prematch baseline fields prematch_winprob_home / prematch_winprob_away, and winprob_dip. Includes sourcing (de-vigged live moneyline, Pinnacle-anchored with cross-book consensus fallback), grading honesty (Brier ~0.02–0.06; momentum is a trend/display signal with no measurable predictive value — explicitly warned against building betting logic on it), and absence semantics (fields omitted when not computable — live tennis with a fresh live moneyline only, no null placeholders).
  2. WS-vs-SSE gamestate delivery semantics (gamestate.mdx, stream.mdx) — the two transports were documented as if they behaved identically; they do not:
    • WebSocket: incremental gamestate:update (changed rows only) + explicit gamestate:removed id lists + resumable via resume=true&from_seq=N.
    • SSE: every gamestate:update re-emits the full current slate; there is no gamestate:removed on SSE; the channel is not resumable (Last-Event-ID replay covers odds only — gamestate frames carry no id: line, and the resumed: false / fallback_reason: "channel_unsupported" ack only appears when a reconnect actually presents a Last-Event-ID).
    • New client-pattern guidance: merge deltas on WS, replace wholesale on SSE (else ended events linger forever), plus the filtered-stream edge case where a zero-match cycle sends no gamestate:update at all, so clients should expire unrefreshed rows when heartbeats continue but updates stop.
  3. Replay-buffer correction (websocket.mdx x2) — the documented "2-minute (max 2000 events)" replay buffer is actually TTL-bound at roughly ~5 minutes, best-effort with entry/byte caps; corrected in both the API reference and the streaming guide.

Why

Findings from the 2026-07-03 gamestate architecture end-to-end review (wiki: operations/2026-07-03 Gamestate Architecture End-to-End Review.md). The review traced gamestate from adapters through sharp-api-go to both streaming transports and found the public docs diverged from observed production behavior in exactly these places. An SSE consumer following the previous docs would implement delta-merging and wait for gamestate:removed frames that never arrive, leaving ended matches stuck in local state; the win-prob fields were shipping to Enterprise customers entirely undocumented.

Includes review fixes from the same session (SSE reconnect ack gating, filtered empty-slate caveat, ~5-min WS replay wording, prematch winprob fields).

Rollout

Docs-only change — no code, no new env flags, nothing gated. Merge is behavior-neutral for the API itself; the only effect of merging is the Vercel docs-site deploy publishing these pages. Nothing to activate.

Testing

Repo has no pytest/go suites; the deploy gate (deploy-prod.yml validate job) is typecheck + build + link check, run locally from the worktree:

npm run typecheck   # tsc --noEmit — pass
npm run build       # stamp-openapi + generate-sitemap + next build + fix-locale-links + fix-subheading-anchors + pagefind — pass

Build-side regeneration churn (public/openapi.json / openapi-version.json timestamp stamps, sitemap.xml lastmod bumps) was discarded rather than committed — this PR intentionally contains only the four content .mdx files, so the openapi-version-check gate does not trigger.

Sibling PRs

Same review produced two api-adapters gamestate PRs (league-prefix fragment collapse — api-adapters#2014 — and tier0 authority/guards). Those two touch the same api-adapters file in different regions and can merge in either order; this docs PR is independent of both — it documents current production behavior, not anything those shadow-gated PRs change. No ordering constraint.

Type: docs

🤖 Generated with Claude Code

sharp-resolver[bot] and others added 4 commits July 3, 2026 00:58
…mestate payload

Adds a "Win probability (tennis)" subsection + sport-specific table row
for the live-only win_prob_home / win_prob_away / win_prob_book /
win_prob_at / momentum / momentum_window_s fields that have been on the
gamestate wire since the momentum meter shipped (sharp-api-go
gamestate_winprob.go) but were never in the customer docs (2026-07-03
gamestate architecture review, F8).

States the ground truth precisely: tennis-only today; server-computed
at serve time from the de-vigged live moneyline (Pinnacle anchor →
consensus median → single-book fallback), identical on REST + WS + SSE;
present only while live with a fresh moneyline, omitted otherwise (no
null placeholders); win_prob calibrated (Brier ~0.02-0.06 in ongoing
grading); momentum explicitly framed as a UI/trend signal with no
predictive value. Example uses clearly generic placeholder players.

en locale only — de/es/pt-BR sync is a manual translate-content.mjs
pass (not CI-automated), to be run at ship time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…removed+resume vs full-slate re-emit

The gamestate streaming section previously described one merged model
(snapshot -> changed rows -> gamestate:removed) that is only true for
WebSocket; the 2026-07-03 gamestate architecture review (F7) flagged
the real transport asymmetry as deliberate but unwritten.

gamestate.mdx: new "Delivery semantics: WebSocket vs SSE" table — WS =
incremental gamestate:update frames + gamestate:removed id lists,
resumable via resume=true&from_seq=N (best-effort ~2-min replay
buffer); SSE = full current slate re-emitted per update cycle, no
removed events (removal = absence from the next slate), not resumable
(Last-Event-ID covers odds only; reconnects re-bootstrap with
resumed:false + fallback_reason:"channel_unsupported" and a fresh
gamestate:snapshot). Plus a client-pattern callout: keyed map replaced
wholesale on SSE, deltas applied on WS.

stream.mdx: drop the incorrect gamestate:removed from the SSE channel
table and add gamestate:snapshot / gamestate:update event sections
(full-slate warning, no id: lines — matches the non-resumable wire).

en locale only — de/es/pt-BR sync is a manual translate-content.mjs
pass (not CI-automated), to be run at ship time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pty-slate caveat, ~5-min WS replay, prematch winprob fields

- SSE gamestate reconnect: resumed:false + fallback_reason:"channel_unsupported"
  appear on `connected` only when the reconnect presents a Last-Event-ID (e.g. on
  the `all` channel, or a hand-set header). Gamestate frames and heartbeats carry
  no SSE id: line, so a plain EventSource on the gamestate channel reconnects
  without a Last-Event-ID and gets neither key — the fresh gamestate:snapshot
  re-bootstrap is the unconditional part. (sse.go resumeAttempted gating;
  streaming.go sseClientTrySend emits no id: line.) Fixed in gamestate.mdx +
  stream.mdx.
- WS replay buffer: "~2-minute window (2000 events)" was pre-#388 sizing — the
  default is TTL-bound at 300s (10k entries / 64 MiB caps, replay.go), no prod
  override. Say ~5 minutes in gamestate.mdx and fix the same stale figure at its
  source (api-reference/websocket.mdx:635/680, streaming/websocket.mdx:141/150).
- Document prematch_winprob_home/away + winprob_dip — shipped alongside the
  win_prob fields since PR#1070 (gamestate_winprob.go), emitted only when a
  prematch baseline was captured before go-live.
- "well-calibrated" -> "sharp ... with a known conservative tilt": ongoing
  grading shows favorite underconfidence ~6.5pp / calibration gap ~0.13; the
  Brier range stays.
- Filtered SSE streams receive no gamestate:update at all on cycles matching
  zero events (streaming.go handleGamestateUpdate skips empty selections), so
  ended-event-stops-appearing fails for the last matching event — documented
  the edge case + client mitigation in gamestate.mdx + stream.mdx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecision 2026-07-03)

Matches enforcement PR sharp-api-go#1168 — Game State add-on keys
receive every other gamestate field; the nine win-prob family fields
are omitted for non-Enterprise keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Mlaz-code Mlaz-code merged commit 32d2e8a into main Jul 3, 2026
@Mlaz-code Mlaz-code deleted the docs/gamestate-winprob-streaming-semantics branch July 3, 2026 07:06
@Mlaz-code

Copy link
Copy Markdown
Collaborator Author

Merge gate (auto-classified): gate=docs — auto-classified by the gate-log enforcer (14min after merge, no Merge gate: audit comment found). Reviewer agent: please correct if wrong by replying with Merge gate: gate=<correct-word> — <evidence>. The audit log keys on the first such token; your correction overrides this auto-classified guess.

Posted by scripts/gate-log-enforcer.py because instruction-level rules about the gate= template aren't binding (0/N adoption); structural enforcement fills the log either way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant