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):
| sport | catalogue matches | classified | linked | link rate on classified |
|---|---|---|---|---|
| basketball (1) | 46 | 0 | 0 | — |
| tennis (6) | 555 | 0 | 0 | — |
| soccer (10) | 5,404 | 1,594 | 712 | 44.7% |
| cricket (27) | 369 | 136 | 69 | 50.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=NULLat 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.
- 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.
- 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.
- 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). - A visibility edit may not declassify.
tournamentConfigUpdateSchema(backend/src/routes/admin.ts:4869-4878) makestiera required key with a nullable value whileisVisibleis 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 producedis_visible=false, tier=NULLon 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.