refactor(esports): a game is a game, not a game page - #905
Merged
Conversation
ExtraToast
force-pushed
the
refactor/a-game-is-a-game
branch
4 times, most recently
from
August 31, 2026 15:33
3124270 to
59dfdc3
Compare
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
force-pushed
the
refactor/a-game-is-a-game
branch
from
August 31, 2026 15:52
59dfdc3 to
3d9cfa4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #904.
The esports module called a game a game page —
GamePage,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
GamePage→Game,game_page→game, and the repository, service and IT follow. The column holdingVALORANTbecomescode, which is what every service in the module already called it —codes(),codeFor(name), and acontentsOfthat readrequireGame(game).gameinto a variable namedcode. V94 had already named its indexuk_game_page_code.The row carries three string identities and only one is a name:
codeVALORANTteam_season,season_gameanduser_game_accountpoint atnameValorantslugvalorantThe three referencing columns keep the name
game: they answer which game. The contract follows the same split —codeon a game's own response,gameon anything pointing at one (SeasonGameResponse,RosterEntryResponse,GameAccountResponse,?game=,/games/{game}).games.csvnames its first columncode; the seed files that reference a game keep theirs.EsportsPageResponsecarries no name, slug or art — it is a game's rosters for one season plus the seasons that have them, so it isGameRostersResponse.GameSeasonResponsewould have sat one word-order away from theSeasonGameResponsethat already exists and means something else.EsportsPageQueryService→EsportsQueryService,EsportsPageView.kt→EsportsViews.kt.nameOnTeamPages→nameOnRosters. 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.
@Schemadescriptions lose their rendering clauses.Deliberately untouched: the
board,cohort,jobs,oidcandsecuritypage 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_atstaysAn earlier revision of this branch dropped it as vestigial. That was wrong and is reverted:
GameextendsAuditedAutoIdEntity, which extendsAuditedSoftDeleteEntity, 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
Gameonto an audited base class without soft delete, which does not exist yet and would be a new type inshared/modelunder 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.tsagainst main'''sindex.ts/sdk.gen.tsand typechecking failed. This is now rebased on current main with the artefacts regenerated at each commit.Two consequences worth noting for review:
ResponseStatusExceptions with typed refusals carrying codes. Those are taken as-is, on the renamed identifiers.esports/refusals.ts,EsportsRefusal.summaryis now developer-facing — its own comment says so. So two summaries were scrubbed after all:"A game'''s page needs an address."→"A game needs an address."and"That address belongs to the esports index."→"...the esports listing.". The sentences a person actually reads, inrefusals.ts, are untouched.One
pageremains in the backend esports module on purpose:@field:NotBlank(message = "A game'''s page needs an address")inEsportsRequests. 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 localblueshell-testfails 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); frontendtypecheck,lint --max-warnings=0,test:unit; and the Playwright specs that caught theGameBySlugbug —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-testschema 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.