Skip to content

refactor(esports): a game is a game, not a game page - #905

Merged
ExtraToast merged 4 commits into
mainfrom
refactor/a-game-is-a-game
Aug 31, 2026
Merged

refactor(esports): a game is a game, not a game page#905
ExtraToast merged 4 commits into
mainfrom
refactor/a-game-is-a-game

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #904.

The esports module called a game a game pageGamePage, game_page, and every service, response and comment that followed from it. That is the frontend's word. A game is a game: the row says what the game is, and it says so whether or not anything ever draws it.

The rename

GamePageGame, game_pagegame, and the repository, service and IT follow. The column holding VALORANT becomes code, which is what every service in the module already called it — codes(), codeFor(name), and a contentsOf that read requireGame(game).game into a variable named code. V94 had already named its index uk_game_page_code.

The row carries three string identities and only one is a name:

column example nature
code VALORANT immutable; what team_season, season_game and user_game_account point at
name Valorant the human name, editable
slug valorant the address, editable

The three referencing columns keep the name game: they answer which game. The contract follows the same split — code on a game's own response, game on anything pointing at one (SeasonGameResponse, RosterEntryResponse, GameAccountResponse, ?game=, /games/{game}). games.csv names its first column code; the seed files that reference a game keep theirs.

EsportsPageResponse carries no name, slug or art — it is a game's rosters for one season plus the seasons that have them, so it is GameRostersResponse. GameSeasonResponse would have sat one word-order away from the SeasonGameResponse that already exists and means something else. EsportsPageQueryServiceEsportsQueryService, EsportsPageView.ktEsportsViews.kt.

nameOnTeamPagesnameOnRosters. A roster is a thing the association keeps; a page is one way of looking at one. The checkbox keeps its wording — it is addressed to somebody looking at a page.

Prose

Comments described backend decisions as pages, slices, bands, strips, dialogs, a season switcher and the esports index. They now name the act or the caller, and call the frontend the frontend where it is genuinely the other side of a decision. @Schema descriptions lose their rendering clauses.

Deliberately untouched: the board, cohort, jobs, oidc and security page comments; file/api/PublicImages; FileType's general prose; all real pagination; and the user-facing refusal sentences, on the same grounds as the consent label.

deleted_at stays

An earlier revision of this branch dropped it as vestigial. That was wrong and is reverted: Game extends AuditedAutoIdEntity, which extends AuditedSoftDeleteEntity, so the column is mapped — it is only never filtered on. Dropping it left Hibernate selecting a column that no longer existed, which is what failed 96 integration tests here.

Removing it properly means moving Game onto an audited base class without soft delete, which does not exist yet and would be a new type in shared/model under ADR-020 governance. That is a separate decision, not a rename.

Rebased onto #903 and #906

Both landed on main while this was open, and both regenerate the SDK, so the first push merged my types.gen.ts against main'''s index.ts/sdk.gen.ts and typechecking failed. This is now rebased on current main with the artefacts regenerated at each commit.

Two consequences worth noting for review:

One page remains in the backend esports module on purpose: @field:NotBlank(message = "A game'''s page needs an address") in EsportsRequests. Bean Validation messages have no frontend mapping, so that string is what a person sees.

Migrations edited in place

V88 and V92–V95 are unreleased. V92 and V94 are not terse DDL but arguments for why the table exists, and a V99 rename would not retract the word — it would tell a second story over the top of it. This assumes #598 has not merged when this lands; if it has, this needs redoing as a forward migration.

Local databases carrying these migrations need dropping and recreating — the checksums changed. Do not rely on clean-on-validation-error; on this Flyway version a stale local blueshell-test fails validation rather than self-healing. CI provisions a fresh schema per run and is unaffected.

Verification

Run locally: API compileKotlin / compileTestKotlin / compileIntegrationTestKotlin; scripts/generate-openapi-local.sh --blueshell-only (spec and SDK regenerate to no diff); frontend typecheck, lint --max-warnings=0, test:unit; and the Playwright specs that caught the GameBySlug bug — esports-game-address, esports-game-records, esports-game-edit, esports-game-remove, 21 passed.

Integration tests could not be run locally (the workstation's blueshell-test schema collides with unrelated work and port 3306 is held by another project's MySQL); CI is the signal for those.

Reviewing

Commit by commit is much easier than the combined diff — the rename, the consent flag, the prose and the ADR are separate.

🤖 Generated with Claude Code


Diff breakdown added removed, scaled to the largest row.

api                                               +386   -386   60
  production         █████████████░░░░░░░░░░░░░   +269   -269   49
  integration tests  ██████░░░░░                  +116   -116   10
  test fixtures      █░                             +1     -1    1
  generated          █░                             +1     -1    1  ~

frontend                                          +115   -115   19
  production         ████░░░░                      +86    -86   13
  unit tests         █░                             +6     -6    2
  e2e tests          █░                            +23    -23    4
  generated          ███░░░                        +67    -64    3  ~

docs                                               +98     -0    2
  docs               █████                         +98     -0    2

──────────────────────────────────────────────────────────────────
production                                        +355   -355
tests                                             +146   -146  0.41 test lines per prod line
total (hand-written)                              +599   -501  81 files
~ generated (excluded)                             +68    -65  4 files

The entity was `GamePage` and the table was `game_page`, so the services, the
responses and the read models followed. That is the frontend's word for it. A
game is a game: the row says what the game is, and it says so whether or not
anything ever draws it.

The row carries three string identities and only one of them is a name, so the
column that held `VALORANT` is now `code`, beside the `name` a reader sees and
the `slug` an address answers to. Every service in the module already said as
much — `codes()`, `codeFor(name)`, and a `contentsOf` that read
`requireGame(game).game` into a variable it called `code`. Only the column
disagreed. V94 had already named its index `uk_game_page_code`.

The three columns pointing at it keep the name `game`: they answer which game,
which is what an address to another table says.

The contract follows the same rule. On a game's own response the field is its
identity, so `code`; on anything referencing a game — a season's games, a roster
entry, a member's account, `?game=`, `/games/{game}` — it stays `game`.
`games.csv` names its first column `code` for the same reason, while the seed
files that point at a game keep theirs.

`EsportsPageResponse` carries no name, no slug, no art: it is a game's rosters
for one season and the seasons that have them, so it is `GameRostersResponse`.
Calling it `GameSeasonResponse` would have put it one word apart from the
`SeasonGameResponse` that already exists and means something else.

Migrations are rewritten rather than followed by a rename. V88 and V92 to V98
are unreleased, and V92 and V94 are not terse DDL but arguments for why the
table exists — a later rename would not retract the word, only tell a second
story over the top of it.
`nameOnTeamPages` named the consent after the surface it happened to be read
on. What the member is agreeing to is that their real name may stand beside
their handle in a roster — a roster is a thing the association keeps, and a page
is one way of looking at one. It is `nameOnRosters`, in the column, the entity,
the contract and the reads that cross from esports into the user module.

The checkbox keeps its wording. "Show my name next to my handle on the esports
team pages" is addressed to somebody looking at a page, and telling them about
rosters instead would be worse for them. The identifier names the fact; the
label describes where they will see it.

V88 is unreleased, so the column does not exist anywhere yet and no consent is
being reinterpreted by renaming it.
The comments described backend decisions in the frontend's words: pages, slices,
bands, strips, dialogs, a season switcher, the esports index. Each one names a
thing that lives in a component and can be renamed there without anybody
touching this module, which is how a comment goes stale without going wrong.

They now name what the sentence is actually about — the public read, a caller,
a visitor, an edit, a season's games, the season list. Where the frontend is
genuinely the other side of a decision it is called the frontend, which is not
its furniture but its name.

The `@Schema` descriptions lose their rendering clauses. "The game's own image,
drawn in the slice for it on the esports index" describes one client's layout in
a document every client reads; what the field is is the whole of the contract's
business.

Two spots in `shared/enums` come along because they are about these records and
nothing else: `FileType`'s per-kind notes for a game's and a team's art, and
`TeamRole`. `FileType`'s general prose about what the site draws stays as it is,
along with the `board`, `cohort`, `jobs`, `oidc` and `security` comments and
`file/api/PublicImages` — those are not about esports, and Spring's pagination
was never our word to begin with.

The refusal sentences a person reads are left alone, on the same grounds the
consent checkbox was: they are addressed to somebody looking at a page.
"Page" got into the esports module because nothing said it could not, and it
spread by ordinary consistency once it was there: a `GamePage` entity earns a
`GamePageService`, whose KDoc says "every game has a page", which is then the
module's vocabulary to the next reader.

The rename is worthless if the reasoning behind it is not written down. Records
are named for what they are rather than where they appear; identity is `code`
and the human name is `name`; comments name the act or the caller rather than
the widget, and call the frontend the frontend when it is genuinely the other
side of a decision.

The ADR records what the rule does not cover — user-facing copy, real
pagination, prose elsewhere that mentions a page for its own reasons — and what
it cost, which is that some comments can no longer justify a decision by naming
the component that forced it. That cost is the part most likely to be undone by
somebody restoring the more vivid sentence, so it is argued rather than assumed.
@ExtraToast
ExtraToast force-pushed the refactor/a-game-is-a-game branch from 59dfdc3 to 3d9cfa4 Compare August 31, 2026 15:52
@ExtraToast
ExtraToast merged commit 9b85848 into main Aug 31, 2026
25 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 31, 2026
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.

The backend says game, not game page

1 participant