Skip to main content

ADR-0032 — Tournament classification is the data plane's first gate

  • Status: Accepted
  • Date: 2026-08-27
  • Related: ADR-0020 (admin tournament assignment is explicit and durable), ADR-0021 (missing fixture start time is quarantined), docs/architecture/data-plane-integration.md §1

Context​

Every account of "why does this fixture show no score" in this codebase has so far been told as a fixture-linking problem. It is not, or not first.

selectUnlinkedFixtures (backend/src/services/thesports/fixtureLinkService.ts:1016) offers a fixture to the matcher only if its tournament carries a non-null tier on catalogue_display_configs. A non-null tier is this codebase's single definition of "classified", and an unclassified tournament is not shown to a player at all. So the matcher is never asked about an unclassified fixture, no link is ever written for it, and no score can ever be overlaid on it — regardless of how rich the provider feed is.

Measured on dev, 2026-08-27 ~19:00 UTC, current diary window (−3d … +2d):

sportcatalogue matchesclassifiedlinkedlink rate on classified
basketball (1)4600—
tennis (6)55500—
soccer (10)5,4041,59471244.7%
cricket (27)3691366950.7%

Tennis is 31/31 entitled — the only sports where we hold ALL DATA are cricket and tennis — with odds, per-set serve/return statistics, point-by-point rallies and full draw brackets, all of it already decoded and typed on the frontend. None of it has ever reached a screen, and the reason is not the feed and not the join.

Three incidents already recorded in this codebase are the same gate:

  • Ind v Sri Lanka, 2026-08-26. An admin write set is_visible=false, tier=NULL at 13:17:40 UTC; the whole Test series left the player view while 27 orders had been placed on it. Restored 2026-08-27 05:33:50 UTC.
  • ~36% curation withholding, prod, 9h/59 events: largest sample 763 fixtures → 118 unclassified + 161 admin-hidden → 484 rendered.
  • Basketball's permanently wasted matcher work, recorded in the selection function's own comment: "we classify no basketball tournament, so every comparison this job made for that sport was work for a card that does not exist."

streamManager.ts:138-144 fail-closed skips an unclassified fixture before any Betfair membership opens, so the same gate also throttles market streaming.

Decision​

Classification, fixture identity and feed coverage are three gates in series, in that order, and every diagnosis, dashboard and ticket must name which one it is about.

  1. The classification gate is upstream and is not a data-plane concern to fix. The data plane does not widen it, work around it, or resolve past it. Resolving an unclassified fixture buys a link nobody can see at the cost of scoring every fixture in the sport, and the candidate-set reduction stays.
  2. A sport with zero classified tournaments is reported as a classification failure, not as a feed or link failure. "No scores on tennis" is a curation backlog, and must be surfaced as one.
  3. Link health is measured as a rate on classified fixtures, per sport, with a date. Never as a cross-sport row-count ratio — soccer carries ~15× the fixtures, and the ratio inverted the true conclusion for cricket (ADR text §Context; data-plane-integration.md §13 C1).
  4. A visibility edit may not declassify. tournamentConfigUpdateSchema (backend/src/routes/admin.ts:4869-4878) makes tier a required key with a nullable value while isVisible is optional, so any caller flipping visibility without re-sending the current tier wipes the tier in the same write. That is the exact mechanism that produced is_visible=false, tier=NULL on Ind v Sri Lanka. The two fields must be independently optional, and a tier must only ever be cleared by an explicit, separate intent.

Consequences​

  • The largest available win in the data-plane integration is operational, not engineering: classifying tennis and basketball tournaments lights up two fully decoded sports with no code change at all.
  • Someone must own tournament classification on a cadence, with a visible backlog. Today it is a manual admin action with no worklist, which is why two whole sports have sat at zero.
  • A "no score" bug report is now triaged in a fixed order — classified? linked? covered? — and the first two are one indexed query each.
  • The tier-required-key defect gets its own fix and is not bundled into any data-plane change; it can recur any night until it lands.