R6(a): de-stage code, docs, READMEs; split stage6_test

Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN)
references from comments, doc-comments, service READMEs, the current-state docs
(ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the
.fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage
history.

- Rename the only stage-named identifiers: registerStage8 -> registerSocialOps,
  registerStage11 -> registerLinkOps (gateway transcode).
- Split stage6_test.go: TestEmailLoginFlow -> email_test.go,
  TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go.
- Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged
  .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments).

go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
This commit is contained in:
Ilia Denisov
2026-06-10 16:56:03 +02:00
parent a372343797
commit 8881214213
156 changed files with 749 additions and 778 deletions
+6 -7
View File
@@ -93,13 +93,13 @@ type gameDTO struct {
MoveCount int `json:"move_count"`
EndReason string `json:"end_reason"`
// LastActivityUnix is the lobby sort key: the current turn's start for an active
// game, the finish time once finished (Stage 17).
// game, the finish time once finished.
LastActivityUnix int64 `json:"last_activity_unix"`
Seats []seatDTO `json:"seats"`
}
// moveResultDTO is the outcome of a committed move. Rack carries the actor's refilled rack as
// wire alphabet indices and BagLen the bag size after the draw (R4), so the mover renders the
// wire alphabet indices and BagLen the bag size after the draw, so the mover renders the
// next state from the response without a follow-up state fetch.
type moveResultDTO struct {
Move moveRecordDTO `json:"move"`
@@ -109,15 +109,14 @@ type moveResultDTO struct {
}
// alphabetEntryDTO is one letter of a variant's alphabet (its index, concrete letter and
// tile value), embedded in the state view for display only when the client requests it
// (Stage 13).
// tile value), embedded in the state view for display only when the client requests it.
type alphabetEntryDTO struct {
Index int `json:"index"`
Letter string `json:"letter"`
Value int `json:"value"`
}
// stateDTO is a player's view of a game. Rack carries wire alphabet indices (Stage 13; a
// stateDTO is a player's view of a game. Rack carries wire alphabet indices (a
// blank is engine.BlankIndex). Alphabet is present only when the request asked for it.
type stateDTO struct {
Game gameDTO `json:"game"`
@@ -236,7 +235,7 @@ func moveRecordDTOFrom(m engine.MoveRecord) moveRecordDTO {
}
// moveResultDTOFrom projects a committed move result into its DTO, encoding the actor's rack as
// wire alphabet indices (Stage 13; R4).
// wire alphabet indices.
func moveResultDTOFrom(r game.MoveResult) (moveResultDTO, error) {
rack, err := engine.EncodeRack(r.Game.Variant, r.Rack)
if err != nil {
@@ -251,7 +250,7 @@ func moveResultDTOFrom(r game.MoveResult) (moveResultDTO, error) {
}
// stateDTOFrom projects a player's state view into its DTO, encoding the rack as wire
// alphabet indices (Stage 13). When includeAlphabet is set it also embeds the variant's
// alphabet indices. When includeAlphabet is set it also embeds the variant's
// display table, which the client caches per variant and renders the rack with.
func stateDTOFrom(v game.StateView, includeAlphabet bool) (stateDTO, error) {
rack, err := engine.EncodeRack(v.Game.Variant, v.Rack)
+5 -6
View File
@@ -18,11 +18,10 @@ import (
"scrabble/backend/internal/social"
)
// registerRoutes wires the Stage 6 REST handlers onto the /api/v1 groups. The
// registerRoutes wires the REST handlers onto the /api/v1 groups. The
// internal group is gateway-only (the gateway authenticates and forwards); the
// user group requires X-User-ID; the admin group is reached through the gateway's
// Basic-Auth proxy. This is the representative vertical slice — further domain
// operations follow the same pattern (PLAN.md Stage 6).
// Basic-Auth proxy.
func (s *Server) registerRoutes() {
if s.sessions != nil && s.accounts != nil {
in := s.internal
@@ -32,13 +31,13 @@ func (s *Server) registerRoutes() {
in.POST("/sessions/email/login", s.handleEmailLogin)
in.POST("/sessions/resolve", s.handleResolveSession)
in.POST("/sessions/revoke", s.handleRevokeSession)
// Out-of-app push routing for the platform side-service (Stage 9): the
// Out-of-app push routing for the platform side-service: the
// gateway resolves a recipient's Telegram chat + language + in-app-only flag
// before delivering an out-of-app notification.
in.POST("/push-target", s.handlePushTarget)
}
if s.ratewatch != nil {
// The gateway's periodic rate-limiter rejection summary (R3): feeds the
// The gateway's periodic rate-limiter rejection summary: feeds the
// admin console's throttled view and the high-rate auto-flag.
s.internal.POST("/ratelimit/report", s.handleRateLimitReport)
}
@@ -49,7 +48,7 @@ func (s *Server) registerRoutes() {
u.GET("/stats", s.handleStats)
}
if s.links != nil {
// Account linking & merge (Stage 11). The request step always mails a code;
// Account linking & merge. The request step always mails a code;
// a required merge is revealed only after the code is verified, and the
// irreversible merge is an explicit second step.
u.POST("/link/email/request", s.handleLinkEmailRequest)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
// The /api/v1/user account handlers wire profile editing, email binding and the
// statistics read (Stage 8). They follow handlers_user.go: X-User-ID identity, a
// statistics read. They follow handlers_user.go: X-User-ID identity, a
// domain call, a JSON DTO. Profile editing overwrites the full editable set, so the
// client sends the complete desired profile.
@@ -560,7 +560,7 @@ func (s *Server) consolePostBroadcast(c *gin.Context) {
// consoleThrottled renders the rate-limit observability page: the recent
// gateway-reported throttle episodes (in-memory, reset on a backend restart)
// and the accounts currently carrying the soft high-rate flag (R3).
// and the accounts currently carrying the soft high-rate flag.
func (s *Server) consoleThrottled(c *gin.Context) {
ctx := c.Request.Context()
var view adminconsole.ThrottledView
@@ -595,7 +595,7 @@ func (s *Server) consoleThrottled(c *gin.Context) {
}
// consoleClearHighRateFlag clears the soft high-rate marker — the operator's
// reversible review action (R3).
// reversible review action.
func (s *Server) consoleClearHighRateFlag(c *gin.Context) {
id, ok := s.consoleUUID(c, "/_gm/users")
if !ok {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/google/uuid"
)
// The /api/v1/user/blocks/* handlers wire the per-user block list (Stage 8). A block
// The /api/v1/user/blocks/* handlers wire the per-user block list. A block
// is mutual in effect (the social checks apply it both ways) and severs any
// friendship between the pair. They reuse the friend handlers' targetIDRequest and
// account-ref resolution.
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/google/uuid"
)
// The /api/v1/user/friends/* handlers wire the social friend graph (Stage 8): the
// The /api/v1/user/friends/* handlers wire the social friend graph: the
// befriend-an-opponent request flow, the one-time friend-code path, and the
// friends/incoming lists. They follow handlers_user.go: X-User-ID identity, a domain
// call, a JSON DTO. Account ids are projected to {id, display_name} refs resolved
+9 -10
View File
@@ -12,10 +12,9 @@ import (
"scrabble/backend/internal/game"
)
// The handlers below extend the Stage 6 vertical slice with the remaining game and
// chat operations the UI needs (PLAN.md Stage 7). They follow the same pattern as
// handlers_user.go: X-User-ID identity, the domain service call, a JSON DTO mapped
// from the result.
// The handlers below cover the game and chat operations the UI needs. They follow
// the same pattern as handlers_user.go: X-User-ID identity, the domain service
// call, a JSON DTO mapped from the result.
// hintResultDTO is the top-ranked move plus the remaining hint budget.
type hintResultDTO struct {
@@ -53,7 +52,7 @@ type chatListDTO struct {
}
// exchangeRequest swaps the given rack tiles back into the bag. Tiles are wire alphabet
// indices (Stage 13); a blank is engine.BlankIndex.
// indices; a blank is engine.BlankIndex.
type exchangeRequest struct {
Tiles []int `json:"tiles"`
}
@@ -211,7 +210,7 @@ func (s *Server) handleEvaluate(c *gin.Context) {
}
// handleCheckWord looks a word up in the game's pinned dictionary. The word arrives as
// repeated ?idx= alphabet indices (Stage 13); the backend decodes them to the concrete
// repeated ?idx= alphabet indices; the backend decodes them to the concrete
// word for the lookup and echoes that concrete word back for the client's result cache.
func (s *Server) handleCheckWord(c *gin.Context) {
_, gameID, ok := s.userGame(c)
@@ -242,7 +241,7 @@ func (s *Server) handleCheckWord(c *gin.Context) {
}
// queryIndexes parses repeated integer query parameters (e.g. ?idx=2&idx=0) into a slice.
// It carries a word-check query as alphabet indices on a GET (Stage 13).
// It carries a word-check query as alphabet indices on a GET.
func queryIndexes(c *gin.Context, key string) ([]int, error) {
raw := c.QueryArray(key)
out := make([]int, 0, len(raw))
@@ -326,7 +325,7 @@ type draftTileDTO struct {
Blank bool `json:"blank"`
}
// draftDTO is a player's persisted client-side composition for a game (Stage 17): the
// draftDTO is a player's persisted client-side composition for a game: the
// preferred rack tile order (an opaque client string) and the board tiles laid but not yet
// submitted. The gateway forwards the JSON verbatim; this layer owns its shape.
type draftDTO struct {
@@ -352,7 +351,7 @@ func (d draftDTO) toDomain() game.Draft {
return game.Draft{RackOrder: d.RackOrder, BoardTiles: tiles}
}
// handleGetDraft returns the player's saved composition for a game (Stage 17), or an empty
// handleGetDraft returns the player's saved composition for a game, or an empty
// draft when none is stored.
func (s *Server) handleGetDraft(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
@@ -367,7 +366,7 @@ func (s *Server) handleGetDraft(c *gin.Context) {
c.JSON(http.StatusOK, draftDTOFrom(d))
}
// handleSaveDraft upserts the player's composition for a game (Stage 17). The service
// handleSaveDraft upserts the player's composition for a game. The service
// rejects a non-player with ErrNotAPlayer.
func (s *Server) handleSaveDraft(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
@@ -12,7 +12,7 @@ import (
"scrabble/backend/internal/lobby"
)
// The /api/v1/user/invitations/* handlers wire friend-game invitations (Stage 8):
// The /api/v1/user/invitations/* handlers wire friend-game invitations:
// create a 2-4 player invitation, accept/decline as an invitee, cancel as the
// inviter, and list the open invitations touching the caller. Display names for the
// inviter and invitees are resolved from the account store.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"scrabble/backend/internal/link"
)
// The /api/v1/user/link handlers drive account linking & merge (Stage 11). The
// The /api/v1/user/link handlers drive account linking & merge. The
// request step always mails a code (no pre-send "taken" signal, so a probe cannot
// enumerate registered emails); confirm reveals a required merge only after the
// code is verified; merge performs the irreversible consolidation behind an
@@ -23,7 +23,7 @@ type rateLimitReportEntry struct {
}
// handleRateLimitReport ingests one gateway rejection report into the rate
// watch — the admin console's throttled view and the high-rate auto-flag (R3).
// watch — the admin console's throttled view and the high-rate auto-flag.
// Internal, gateway-only: like sessions/resolve it trusts the network segment.
// Malformed individual entries are skipped by the watch itself.
func (s *Server) handleRateLimitReport(c *gin.Context) {
+1 -1
View File
@@ -58,7 +58,7 @@ func TestResolveSessionRejectsEmptyToken(t *testing.T) {
}
}
// TestRateLimitReportEndpoint covers the internal R3 report route: a malformed
// TestRateLimitReportEndpoint covers the internal report route: a malformed
// body is a 400, a valid report lands in the rate watch with 204.
func TestRateLimitReportEndpoint(t *testing.T) {
watch := ratewatch.New(ratewatch.DefaultConfig(), nil, nil)
+3 -3
View File
@@ -27,7 +27,7 @@ func (s *Server) handleProfile(c *gin.Context) {
}
// submitPlayRequest places tiles in a direction on the player's turn. Each tile's Letter
// is a wire alphabet index (Stage 13); for a blank it is the designated letter's index.
// is a wire alphabet index; for a blank it is the designated letter's index.
type submitPlayRequest struct {
Dir string `json:"dir"`
Tiles []struct {
@@ -39,7 +39,7 @@ type submitPlayRequest struct {
}
// tilesFromRequest maps a play/evaluate request's index-addressed tiles to engine tile
// records for the game's variant (Stage 13: a placed blank carries its designated letter's
// records for the game's variant (a placed blank carries its designated letter's
// index with Blank set). An out-of-range index surfaces as engine.ErrIllegalPlay (HTTP 400).
func tilesFromRequest(variant engine.Variant, req submitPlayRequest) ([]engine.TileRecord, error) {
tiles := make([]engine.TileRecord, 0, len(req.Tiles))
@@ -94,7 +94,7 @@ func (s *Server) handleSubmitPlay(c *gin.Context) {
}
// handleGameState returns the player's view of a game.
// handleHideGame hides a finished game from the caller's own lobby list (Stage 17).
// handleHideGame hides a finished game from the caller's own lobby list.
func (s *Server) handleHideGame(c *gin.Context) {
uid, ok := userID(c)
if !ok {
+10 -10
View File
@@ -50,20 +50,20 @@ type Deps struct {
// func skips the session-readiness check.
SessionsReady func() bool
// Sessions, Accounts and Games are the identity, account and game-domain
// services the Stage 6 REST handlers route to.
// services the REST handlers route to.
Sessions *session.Service
Accounts *account.Store
Games *game.Service
// Social, Matchmaker, Invitations and Emails are the Stage 4 domain services
// the Stage 6 REST handlers route to.
// Social, Matchmaker, Invitations and Emails are the domain services
// the REST handlers route to.
Social *social.Service
Matchmaker *lobby.Matchmaker
Invitations *lobby.InvitationService
Emails *account.EmailService
// Links drives account linking & merge (Stage 11): the /api/v1/user/link
// Links drives account linking & merge: the /api/v1/user/link
// endpoints. A nil Links disables them.
Links *link.Service
// Registry holds the resident dictionaries; the admin console (Stage 10) reads
// Registry holds the resident dictionaries; the admin console reads
// its versions and hot-reloads new ones. DictDir is the dictionary directory a
// reload reads a version subdirectory from. A nil Registry disables the console.
Registry *engine.Registry
@@ -72,7 +72,7 @@ type Deps struct {
// nil when BACKEND_CONNECTOR_ADDR is unset (broadcasts show a "not configured"
// notice).
Connector *connector.Client
// RateWatch ingests the gateway's rate-limiter rejection reports (R3): the
// RateWatch ingests the gateway's rate-limiter rejection reports: the
// admin console's throttled view + the high-rate auto-flag. A nil RateWatch
// disables the internal report endpoint and the console view.
RateWatch *ratewatch.Watch
@@ -196,16 +196,16 @@ func (s *Server) UserGroup() *gin.RouterGroup { return s.user }
// InternalGroup returns the gateway-facing internal route group.
func (s *Server) InternalGroup() *gin.RouterGroup { return s.internal }
// Social returns the social domain service for the handlers added in Stage 6.
// Social returns the social domain service for the handlers.
func (s *Server) Social() *social.Service { return s.social }
// Matchmaker returns the in-memory matchmaking pool for the Stage 6 handlers.
// Matchmaker returns the in-memory matchmaking pool for the handlers.
func (s *Server) Matchmaker() *lobby.Matchmaker { return s.matchmaker }
// Invitations returns the friend-game invitation service for the Stage 6 handlers.
// Invitations returns the friend-game invitation service for the handlers.
func (s *Server) Invitations() *lobby.InvitationService { return s.invitations }
// Emails returns the email confirm-code service for the Stage 6 handlers.
// Emails returns the email confirm-code service for the handlers.
func (s *Server) Emails() *account.EmailService { return s.emails }
// Handler returns the underlying HTTP handler. It lets tests drive the server