1466 lines
62 KiB
Markdown
1466 lines
62 KiB
Markdown
# Game Lobby Service Implementation Plan
|
||
|
||
This plan has been already implemented and stays here for historical reasons.
|
||
|
||
It should NOT be threated as source of truth for service functionality.
|
||
|
||
## Summary
|
||
|
||
This plan builds `Game Lobby Service` as the platform source of truth for game
|
||
sessions, enrollment, membership, the game start sequence, and the
|
||
platform-wide `Race Name Directory` (RND).
|
||
It integrates synchronously with `User Service` for eligibility checks and
|
||
`Game Master` for runtime registration, and asynchronously with `Runtime Manager`
|
||
via Redis Streams for start jobs, with `Game Master` via Redis Streams for
|
||
runtime updates, and with `User Service` via Redis Streams for user lifecycle
|
||
events (permanent block, account deletion) that trigger cascade release of all
|
||
race names owned by the user.
|
||
|
||
The RND supersedes the simple per-name reservation model of Stage 09. It owns
|
||
two kinds of bindings:
|
||
|
||
- **registered** — platform-unique permanent names owned by one user,
|
||
count-bounded per tariff (`max_registered_race_names`);
|
||
- **reservation** — per-game holding that survives until the game finishes,
|
||
then either escalates to a 30-day `pending_registration` (when capability is
|
||
satisfied) or is released immediately.
|
||
|
||
`User Service` simultaneously drops its single-valued `race_name` concept in
|
||
favour of two stable fields: `user_name` (immutable auto-generated handle) and
|
||
`display_name` (mutable free-form text). All anti-fraud canonicalization moves
|
||
from `User Service` into `Lobby RND`.
|
||
|
||
## Global Rules
|
||
|
||
- Keep platform game state strictly in `Game Lobby`; never duplicate it in
|
||
`Game Master` beyond the allowed denormalized snapshot.
|
||
- Preserve all status transition invariants; no transition fires without an
|
||
explicit allowed trigger.
|
||
- Keep the Race Name Directory behind a port interface from the first commit.
|
||
- RND is the sole platform source of truth for in-game `race_name` values.
|
||
`User Service` owns `user_name` (immutable handle) and `display_name` (free
|
||
text), never `race_name`.
|
||
- RND reservations are keyed by `(game_id, canonical_key)`. One user may hold
|
||
the same name simultaneously across multiple active games. A name is
|
||
considered taken for another user when any `registered`, active
|
||
`reservation`, or `pending_registration` by a different user exists on the
|
||
same canonical key.
|
||
- Canonical key logic (lowercase + frozen confusable-pair policy) lives in
|
||
`lobby/internal/domain/racename/policy.go`, not in `User Service`.
|
||
- Post-game capability is evaluated by `Lobby` at `game_finished`:
|
||
`capable = max_planets > initial_planets AND max_population > initial_population`.
|
||
Capable reservations are moved to `pending_registration` with
|
||
`eligible_until = finished_at + 30 days`; incapable reservations are
|
||
released immediately.
|
||
- Registration is user-initiated via `lobby.race_name.register`; it consumes
|
||
one tariff slot. Tariff downgrade never revokes existing registrations.
|
||
- Cascade release (`RND.ReleaseAllByUser`) runs when `Lobby` consumes a
|
||
`permanent_blocked` or `deleted` event from `user:lifecycle_events`.
|
||
- Never publish a notification intent after rolling back business state; always
|
||
publish after successful commit.
|
||
- Use synchronous internal REST only where the architecture document fixes a
|
||
synchronous interaction (`Game Lobby → User Service`,
|
||
`Game Lobby → Game Master` for registration).
|
||
- Use Redis Streams for all other cross-service propagation.
|
||
- Keep enrollment automation and pending-registration expiration idempotent;
|
||
a second tick over the same conditions must produce no side effects.
|
||
- Design Redis-backed stores behind port interfaces to keep a future SQL
|
||
migration and a future dedicated `Race Name Service` possible. Replacing the
|
||
RND adapter must require no domain or service changes.
|
||
|
||
## Suggested Module Structure
|
||
|
||
```text
|
||
lobby/
|
||
├── cmd/
|
||
│ └── lobby/
|
||
│ └── main.go
|
||
│
|
||
├── internal/
|
||
│ ├── app/
|
||
│ │ ├── runtime.go
|
||
│ │ ├── bootstrap.go
|
||
│ │ └── wiring.go
|
||
│ │
|
||
│ ├── config/
|
||
│ │ ├── config.go
|
||
│ │ ├── env.go
|
||
│ │ └── validation.go
|
||
│ │
|
||
│ ├── domain/
|
||
│ │ ├── game/
|
||
│ │ │ ├── model.go
|
||
│ │ │ ├── status.go
|
||
│ │ │ ├── transitions.go
|
||
│ │ │ └── errors.go
|
||
│ │ ├── application/
|
||
│ │ │ ├── model.go
|
||
│ │ │ ├── status.go
|
||
│ │ │ └── errors.go
|
||
│ │ ├── invite/
|
||
│ │ │ ├── model.go
|
||
│ │ │ ├── status.go
|
||
│ │ │ └── errors.go
|
||
│ │ ├── membership/
|
||
│ │ │ ├── model.go
|
||
│ │ │ ├── status.go
|
||
│ │ │ └── errors.go
|
||
│ │ └── common/
|
||
│ │ ├── ids.go
|
||
│ │ └── types.go
|
||
│ │
|
||
│ ├── domain/
|
||
│ │ ├── racename/
|
||
│ │ │ ├── policy.go
|
||
│ │ │ ├── policy_test.go
|
||
│ │ │ └── types.go
|
||
│ │ └── … (game, application, invite, membership, common as before)
|
||
│ │
|
||
│ ├── ports/
|
||
│ │ ├── gamestore.go
|
||
│ │ ├── applicationstore.go
|
||
│ │ ├── invitestore.go
|
||
│ │ ├── membershipstore.go
|
||
│ │ ├── racenamedir.go
|
||
│ │ ├── gameturnstatsstore.go
|
||
│ │ ├── userservice.go
|
||
│ │ ├── userlifecyclestream.go
|
||
│ │ ├── gmclient.go
|
||
│ │ └── runtimemanager.go
|
||
│ │
|
||
│ ├── adapters/
|
||
│ │ ├── redisstate/
|
||
│ │ │ ├── gamestore.go
|
||
│ │ │ ├── applicationstore.go
|
||
│ │ │ ├── invitestore.go
|
||
│ │ │ ├── membershipstore.go
|
||
│ │ │ ├── racenamedir.go
|
||
│ │ │ └── gameturnstatsstore.go
|
||
│ │ ├── racenamestub/
|
||
│ │ │ └── directory.go
|
||
│ │ ├── userservice/
|
||
│ │ │ └── client.go
|
||
│ │ └── gmclient/
|
||
│ │ └── client.go
|
||
│ │
|
||
│ ├── service/
|
||
│ │ ├── creategame/
|
||
│ │ ├── updategame/
|
||
│ │ ├── openenrollment/
|
||
│ │ ├── cancelgame/
|
||
│ │ ├── manualreadytostart/
|
||
│ │ ├── startgame/
|
||
│ │ ├── retrystartgame/
|
||
│ │ ├── pausegame/
|
||
│ │ ├── resumegame/
|
||
│ │ ├── submitapplication/
|
||
│ │ ├── approveapplication/
|
||
│ │ ├── rejectapplication/
|
||
│ │ ├── createinvite/
|
||
│ │ ├── redeeminvite/
|
||
│ │ ├── declineinvite/
|
||
│ │ ├── revokeinvite/
|
||
│ │ ├── removemember/
|
||
│ │ ├── blockmember/
|
||
│ │ ├── registerracename/
|
||
│ │ ├── listmyracenames/
|
||
│ │ ├── capabilityevaluation/
|
||
│ │ ├── getgame/
|
||
│ │ ├── listgames/
|
||
│ │ └── listmemberships/
|
||
│ │
|
||
│ ├── worker/
|
||
│ │ ├── enrollmentautomation/
|
||
│ │ ├── runtimejobresult/
|
||
│ │ ├── gmevents/
|
||
│ │ ├── pendingregistration/
|
||
│ │ └── userlifecycle/
|
||
│ │
|
||
│ ├── api/
|
||
│ │ ├── publichttp/
|
||
│ │ └── internalhttp/
|
||
│ │
|
||
│ ├── telemetry/
|
||
│ └── logging/
|
||
│
|
||
├── api/
|
||
│ ├── public-openapi.yaml
|
||
│ └── internal-openapi.yaml
|
||
│
|
||
├── README.md
|
||
├── PLAN.md
|
||
└── go.mod
|
||
```
|
||
|
||
## ~~Stage 01.~~ Update ARCHITECTURE.md
|
||
|
||
Status: implemented as part of the planning task that produced this file.
|
||
|
||
Goal:
|
||
|
||
- reconcile `ARCHITECTURE.md` with all decisions made during planning
|
||
|
||
Tasks:
|
||
|
||
- Replace the Lobby status model block: remove `enrollment_closed`, add `start_failed`.
|
||
- Add enrollment rules section documenting `min_players`, `max_players`,
|
||
`start_gap_hours`, `start_gap_players`, `enrollment_ends_at`, and the three
|
||
auto-transition paths.
|
||
- Update private game joining rule: redeeming an invite creates active membership
|
||
immediately without a separate owner-approval step.
|
||
- Add Race Name Directory section.
|
||
- Add `Game Master → Game Lobby` runtime snapshot stream to the fixed asynchronous
|
||
interactions list.
|
||
|
||
Exit criteria:
|
||
|
||
- `ARCHITECTURE.md` accurately reflects the status model, enrollment rules, Race
|
||
Name Directory policy, and GM→Lobby transport used throughout this plan and README.
|
||
|
||
## ~~Stage 01R.~~ ARCHITECTURE.md — Race Name Directory expansion
|
||
|
||
Status: implemented — see `lobby/docs/stage01R-architecture-rnd-expansion.md`
|
||
and the corresponding updates across §3 User Service, §7 Game Lobby, §8 Game
|
||
Master (new «Runtime snapshot publishing» subsection), §11 Billing Service,
|
||
and «Fixed asynchronous interactions» in `ARCHITECTURE.md`.
|
||
|
||
Revision of Stage 01 for the two-tier Race Name Directory and the adjacent
|
||
`User Service` refactor.
|
||
|
||
Tasks:
|
||
|
||
- Rewrite §7 Race Name Directory section: registered vs reservation, canonical
|
||
key with confusable-pair policy hosted in Lobby, capability gating at game
|
||
finish, 30-day post-game registration window, cascade release on
|
||
`permanent_block` / `DeleteUser`.
|
||
- Update §3 User Service section: remove `race_name` from owned state; add
|
||
`user_name`, `display_name`, `permanent_block` sanction, `DeleteUser`
|
||
endpoint, `max_registered_race_names` in eligibility snapshot.
|
||
- Update §7 and §8: `runtime_snapshot_update` in `gm:lobby_events` carries
|
||
`player_turn_stats` (`planets`, `population`, `ships_built` per user); Lobby
|
||
maintains per-game/per-user stats aggregate.
|
||
- Update §11 Billing Service: tariff changes affect only *new* registrations.
|
||
- Add `User Service → Game Lobby` to «Fixed asynchronous interactions» as
|
||
`user:lifecycle_events` (permanent_blocked, deleted).
|
||
|
||
Exit criteria:
|
||
|
||
- `ARCHITECTURE.md` matches the locked RND design; no contradictions with
|
||
`lobby/README.md` or `user/README.md`.
|
||
|
||
## ~~Stage 02.~~ Freeze Game Record Vocabulary
|
||
|
||
Status: implemented — see `lobby/README.md` sections Game Record Model through
|
||
Enrollment Rules.
|
||
|
||
Goal:
|
||
|
||
- eliminate all ambiguity in the game entity before writing Go code
|
||
|
||
Tasks:
|
||
|
||
- Confirm all game record fields (names, types, validation rules) in `README.md`.
|
||
- Confirm the full status set and every allowed transition with its trigger.
|
||
- Confirm enrollment auto-transition logic (deadline, gap) in writing.
|
||
- Confirm field immutability rules: which fields are editable in which statuses.
|
||
|
||
Exit criteria:
|
||
|
||
- `lobby/README.md` sections Game Record Model, Status vocabulary,
|
||
Status transition table, and Enrollment Rules contain no unresolved questions.
|
||
|
||
## ~~Stage 03.~~ Freeze Invite, Application, and Membership Vocabulary
|
||
|
||
Status: implemented — see `lobby/README.md` sections Application Lifecycle,
|
||
Invite Lifecycle, Membership Model, and Race Name Directory.
|
||
|
||
Goal:
|
||
|
||
- lock the three participant entity schemas before writing persistence code
|
||
|
||
Tasks:
|
||
|
||
- Confirm all fields for `Application`, `Invite`, and `Membership` in `README.md`.
|
||
- Confirm state machines and allowed transitions for each entity.
|
||
- Confirm that public games use applications only and private games use invites only.
|
||
- Confirm Race Name Directory port interface signature and stub behavior.
|
||
- Confirm that `lobby.invite.revoked` and `lobby.invite.declined` produce no
|
||
notification in v1.
|
||
|
||
Exit criteria:
|
||
|
||
- `lobby/README.md` sections Application Lifecycle, Invite Lifecycle, Membership
|
||
Model, and Race Name Directory contain no unresolved questions.
|
||
- Notification intent shapes in `README.md` are consistent with the frozen catalog
|
||
in `notification/README.md`.
|
||
|
||
## ~~Stage 03R.~~ README.md — Two-tier RND + stats + new APIs
|
||
|
||
Status: implemented — see `lobby/docs/stage03R-readme-rnd-surface.md` and
|
||
`lobby/README.md` sections Race Name Directory, Membership Model, Runtime
|
||
Snapshot, Notification Contracts, Error Model, Configuration, Redis Logical
|
||
Model, and Observability.
|
||
|
||
Revision of Stage 03.
|
||
|
||
Tasks:
|
||
|
||
- Rewrite §Race Name Directory: port interface with `Canonicalize`, `Check`,
|
||
`Reserve(game_id, user_id, race_name)`, `ReleaseReservation`,
|
||
`MarkPendingRegistration`, `ExpirePendingRegistrations`, `Register`,
|
||
`ListRegistered`, `ListPendingRegistrations`, `ListReservations`,
|
||
`ReleaseAllByUser`; new sentinel errors.
|
||
- Update §Membership Model to store `canonical_key` alongside `race_name`.
|
||
- Update §Runtime Snapshot: add `player_turn_stats` (initial + current
|
||
`planets`, `population`, `ships_built` per user). Lobby caches aggregates
|
||
under `lobby:game_turn_stats:<game_id>:<user_id>`.
|
||
- Add §Race Name Registration flow:
|
||
- capability evaluation at `game_finished`;
|
||
- `pending_registration` window = 30 days;
|
||
- `lobby.race_name.register` message type with tariff + capability checks;
|
||
- fast-path self-service read `lobby.race_names.list`.
|
||
- Extend §Notification Contracts with
|
||
`lobby.race_name.registration_eligible`, `lobby.race_name.registered`,
|
||
`lobby.race_name.registration_denied` (optional).
|
||
- Extend §Error Model with `race_name_registration_quota_exceeded`,
|
||
`race_name_pending_window_expired`, `race_name_capability_not_met`,
|
||
`race_name_permanent_blocked`.
|
||
- Extend §Configuration: `LOBBY_RACE_NAME_EXPIRATION_INTERVAL` default `1h`,
|
||
`LOBBY_PENDING_REGISTRATION_TTL_HOURS` default `720`,
|
||
`LOBBY_USER_LIFECYCLE_STREAM` default `user:lifecycle_events`,
|
||
`LOBBY_RACE_NAME_DIRECTORY_BACKEND` default `redis`.
|
||
- Extend §Redis Logical Model with RND keys
|
||
(`lobby:race_names:registered:<canonical_key>`,
|
||
`lobby:race_names:reservations:<game_id>:<canonical_key>`,
|
||
`lobby:race_names:user_registered:<user_id>`,
|
||
`lobby:race_names:user_reservations:<user_id>`,
|
||
`lobby:race_names:pending_index`,
|
||
`lobby:race_names:canonical_lookup:<canonical_key>`,
|
||
`lobby:game_turn_stats:<game_id>:<user_id>`).
|
||
- Extend §Observability with `lobby.race_name.outcomes`,
|
||
`lobby.pending_registration.expirations`,
|
||
`lobby.user_lifecycle.cascade_releases`.
|
||
|
||
Exit criteria:
|
||
|
||
- `lobby/README.md` describes the full RND surface; downstream code stages can
|
||
reference it without further ambiguity.
|
||
|
||
## ~~Stage 04.~~ Define OpenAPI Contracts
|
||
|
||
Status: implemented — see `lobby/api/public-openapi.yaml`,
|
||
`lobby/api/internal-openapi.yaml`, and `lobby/docs/stage04-openapi-decisions.md`.
|
||
|
||
Goal:
|
||
|
||
- produce stable REST contract files before wiring HTTP handlers
|
||
|
||
Tasks:
|
||
|
||
- Add `lobby/api/public-openapi.yaml` covering all message types from the
|
||
message type catalog in `README.md`.
|
||
- Add `lobby/api/internal-openapi.yaml` covering GM registration and admin
|
||
endpoints.
|
||
- Freeze request and response shapes for all routes.
|
||
- Document authorization expectations per route (admin, owner, any member, etc.).
|
||
|
||
Exit criteria:
|
||
|
||
- both OpenAPI files are syntactically valid and cover every route in the message
|
||
type catalog.
|
||
|
||
## ~~Stage 05.~~ Freeze Notification Intent Publishing Rules
|
||
|
||
Status: implemented — see `lobby/README.md` Notification Contracts section and
|
||
the existing `galaxy/notificationintent` module.
|
||
|
||
Goal:
|
||
|
||
- confirm all notification triggers before service and worker code touches
|
||
`galaxy/notificationintent`
|
||
|
||
Tasks:
|
||
|
||
- Map every trigger in `README.md` Notification Contracts to the correct
|
||
constructor in `galaxy/notificationintent`.
|
||
- Confirm that `NewPublicLobbyApplicationSubmittedIntent` is the only path for
|
||
`lobby.application.submitted` in v1.
|
||
- Confirm `lobby.invite.expired` is published per-invite (not batched into one
|
||
intent).
|
||
|
||
Exit criteria:
|
||
|
||
- every notification trigger has an identified constructor in
|
||
`galaxy/notificationintent`; no new constructor is needed for v1.
|
||
|
||
## ~~Stage 06.~~ Module Skeleton
|
||
|
||
Status: implemented — see `lobby/cmd/lobby`,
|
||
`lobby/internal/{config,logging,telemetry,app,api/publichttp,api/internalhttp}`,
|
||
and `lobby/docs/stage06-skeleton-decisions.md`.
|
||
|
||
Goal:
|
||
|
||
- create the runnable service process with no business logic
|
||
|
||
Tasks:
|
||
|
||
- Add `go.mod` dependencies: `redis/go-redis/v9` with `redisotel`,
|
||
the `go.opentelemetry.io/otel` v1.43 stack with `otelhttp`,
|
||
`testcontainers-go` together with `modules/redis`,
|
||
`alicebob/miniredis/v2`, and `stretchr/testify`. The skeleton uses the
|
||
Go standard library `net/http`; no web framework is added. This mirrors
|
||
the dependency set used by `mail` and `notification`.
|
||
- Add `cmd/lobby/main.go` with signal handling and context propagation.
|
||
- Add `internal/config/` with env loading, validation, and `DefaultConfig()`.
|
||
- Add `internal/app/runtime.go`: Redis startup check, structured logger,
|
||
telemetry provider, graceful shutdown, composed through the generic
|
||
`app.Component` lifecycle in `internal/app/app.go` and helpers in
|
||
`internal/app/bootstrap.go`.
|
||
- Add `internal/api/publichttp/` and `internal/api/internalhttp/` routers
|
||
with `GET /healthz` and `GET /readyz` only.
|
||
- Wire both HTTP listeners in `app/runtime.go` through `app.New(...)`.
|
||
|
||
Exit criteria:
|
||
|
||
- `go build ./...` succeeds with no errors.
|
||
- `go test ./...` passes (no tests yet beyond smoke).
|
||
- process starts with a valid Redis address and serves `/healthz` on both ports.
|
||
- process exits cleanly on `SIGTERM`.
|
||
|
||
## ~~Stage 07.~~ Game Domain Model and Redis Store
|
||
|
||
Status: implemented — see `lobby/internal/domain/{common,game}`,
|
||
`lobby/internal/ports/gamestore.go`, `lobby/internal/adapters/redisstate/`,
|
||
and `lobby/docs/stage07-game-store-decisions.md`.
|
||
|
||
Goal:
|
||
|
||
- implement the game entity with status enforcement and Redis persistence
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/domain/game/model.go`: all game fields, value types, constructor
|
||
`New(...)` that validates all required fields.
|
||
- Add `internal/domain/game/status.go`: `Status` type, all status constants,
|
||
`AllowedTransitions` map, `Transition(from, to, trigger)` function that returns
|
||
an error for invalid transitions.
|
||
- Add `internal/domain/game/errors.go`: sentinel and typed errors
|
||
(`ErrNotFound`, `ErrConflict`, `ErrInvalidTransition`).
|
||
- Add `internal/ports/gamestore.go`: port interface
|
||
(`Get`, `GetByStatus`, `Save`, `UpdateStatus`, `UpdateRuntimeSnapshot`).
|
||
- Add `internal/adapters/redisstate/gamestore.go`: Redis implementation using
|
||
JSON serialization.
|
||
- Add `internal/adapters/redisstate/gamestore_test.go`: tests using
|
||
`miniredis`; cover create, get, update, status transition, snapshot update.
|
||
|
||
Exit criteria:
|
||
|
||
- all game store tests pass with `go test ./... -race`.
|
||
- invalid status transitions return an error at the domain level without touching
|
||
the store.
|
||
|
||
## ~~Stage 08.~~ Application, Invite, and Membership Stores
|
||
|
||
Status: implemented — see
|
||
`lobby/internal/domain/{application,invite,membership}`,
|
||
`lobby/internal/ports/{applicationstore,invitestore,membershipstore}.go`,
|
||
`lobby/internal/adapters/redisstate/{codecs_application,codecs_invite,codecs_membership,applicationstore,invitestore,membershipstore}.go`,
|
||
and `lobby/docs/stage08-store-decisions.md`.
|
||
|
||
Goal:
|
||
|
||
- add Redis-backed persistence for the three participant entities
|
||
|
||
Tasks:
|
||
|
||
- Add domain packages: `internal/domain/application/`, `internal/domain/invite/`,
|
||
`internal/domain/membership/` each with `model.go`, `status.go`, `errors.go`.
|
||
- Add port interfaces: `internal/ports/applicationstore.go`,
|
||
`internal/ports/invitestore.go`, `internal/ports/membershipstore.go`.
|
||
- Add Redis adapters for each entity under `internal/adapters/redisstate/`.
|
||
- Add tests for each adapter using `miniredis`.
|
||
- Enforce single active application per user per game at the store level.
|
||
|
||
Exit criteria:
|
||
|
||
- all three entity types persist, load, and list correctly.
|
||
- `go test ./... -race` passes.
|
||
|
||
## ~~Stage 09.~~ Race Name Directory Port and Stub
|
||
|
||
Status: implemented — see `lobby/internal/ports/racenamedir.go`,
|
||
`lobby/internal/adapters/racenamestub/`, `lobby/internal/app/wiring.go`,
|
||
and `lobby/docs/stage09-racenamedir-decisions.md`.
|
||
|
||
Goal:
|
||
|
||
- wire the Race Name Directory abstraction from the start so no code ever
|
||
imports a concrete implementation directly
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/ports/racenamedir.go`: `RaceNameDirectory` interface
|
||
(`Reserve`, `Release`, `Check`) with `ErrNameTaken` sentinel.
|
||
- Add `internal/adapters/racenamestub/directory.go`: in-memory `sync.Map`
|
||
implementation.
|
||
- Wire the stub in `internal/app/wiring.go`.
|
||
- Add unit tests for the stub covering reserve, release, check, and uniqueness
|
||
invariant.
|
||
|
||
Exit criteria:
|
||
|
||
- `racenamestub` tests pass.
|
||
- all future service code refers to `ports.RaceNameDirectory`; no direct
|
||
reference to `racenamestub` outside the wiring layer.
|
||
|
||
## ~~Stage 09R.~~ Race Name Directory: two-tier model and Redis adapter
|
||
|
||
Status: implemented — see `lobby/docs/stage09R-racenamedir-decisions.md`,
|
||
`lobby/internal/ports/racenamedir.go`,
|
||
`lobby/internal/adapters/redisstate/racenamedir.go` (with
|
||
`racenamedir_lua.go` and `codecs_racename.go`),
|
||
`lobby/internal/adapters/racenamestub/directory.go`,
|
||
`lobby/internal/ports/racenamedirtest/suite.go`, and the
|
||
`RaceNameDirectoryConfig` group wired through
|
||
`internal/config/{config,env,validation}.go` and `internal/app/wiring.go`.
|
||
|
||
Replaces Stage 09's port and stub with the two-tier directory. Depends on
|
||
Stage 21 so the confusable-pair policy can be lifted out of `User Service`
|
||
without churn.
|
||
|
||
Tasks:
|
||
|
||
- Rewrite `lobby/internal/ports/racenamedir.go` under the new interface (see
|
||
Stage 03R) with sentinels `ErrNameTaken`, `ErrPendingExpired`,
|
||
`ErrPendingMissing`, `ErrInvalidName`, `ErrQuotaExceeded`.
|
||
- Add `lobby/internal/domain/racename/policy.go`: canonical key generation
|
||
(lowercase + frozen confusable-pair rules ported from
|
||
`user/internal/ports/race_name_policy.go`), `ValidateTypeName` integration
|
||
from `pkg/util`.
|
||
- Implement `lobby/internal/adapters/redisstate/racenamedir.go` atop the Redis
|
||
key layout in Stage 03R; tests use `miniredis`.
|
||
- Rewrite `lobby/internal/adapters/racenamestub/directory.go` against the new
|
||
interface so unit tests that do not need Redis stay fast.
|
||
- Wire adapter selection in `internal/app/wiring.go` via
|
||
`LOBBY_RACE_NAME_DIRECTORY_BACKEND` (`redis` default, `stub` for tests).
|
||
- Port the User Service `RaceNameReservation`/`RaceNamePolicy` tests and their
|
||
golden fixtures to `lobby/internal/domain/racename/`.
|
||
|
||
Exit criteria:
|
||
|
||
- Redis adapter and stub both pass the same behavioural test suite
|
||
(interface-level table tests).
|
||
- Idempotent `Reserve` by the same user under the same game returns nil.
|
||
- `Check` exposes `(taken, holder_user_id, kind)` consistent with Redis state.
|
||
- `MarkPendingRegistration` leaves the existing reservation accessible to
|
||
`ListPendingRegistrations` and to `ExpirePendingRegistrations`.
|
||
- `ReleaseAllByUser` clears every registered, reservation, and pending entry
|
||
for a user atomically (Lua or pipelined transaction).
|
||
- confusable-pair test fixtures from `user/internal/adapters/…` run in the new
|
||
package unchanged.
|
||
|
||
## ~~Stage 10.~~ Game Creation and Draft Management
|
||
|
||
Status: implemented — see `lobby/docs/stage10-game-lifecycle-decisions.md`,
|
||
`lobby/internal/service/{shared,creategame,updategame,openenrollment,cancelgame}`,
|
||
`lobby/internal/ports/idgenerator.go`, `lobby/internal/adapters/{idgen,gamestub}`,
|
||
and the extended `lobby/internal/api/{publichttp,internalhttp}` handlers.
|
||
|
||
Goal:
|
||
|
||
- implement the initial game lifecycle operations with no enrollment logic yet
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/creategame/`: validate all game fields, create game in
|
||
`draft` status, store via `GameStore`.
|
||
- Add `internal/service/updategame/`: allow edits on `draft` and selected fields
|
||
on `enrollment_open`; reject all other statuses.
|
||
- Add `internal/service/openenrollment/`: `draft → enrollment_open` with
|
||
admin/owner authorization check.
|
||
- Add `internal/service/cancelgame/`: cancel from `draft`, `enrollment_open`,
|
||
`ready_to_start`, `start_failed`; reject from `starting`, `running`, `paused`.
|
||
- Wire all four service calls to routes on both HTTP ports.
|
||
- Add service-level tests (in-memory stores, no Redis).
|
||
|
||
Exit criteria:
|
||
|
||
- game creation, update, open-enrollment, and cancel all pass tests.
|
||
- unauthorized callers receive `forbidden`.
|
||
- invalid transition attempts return `conflict`.
|
||
|
||
## ~~Stage 11.~~ Application Flow (Public Games)
|
||
|
||
Status: implemented — see `lobby/docs/stage11-application-flow-decisions.md`,
|
||
`lobby/internal/service/{submitapplication,approveapplication,rejectapplication}`,
|
||
`lobby/internal/ports/{userservice,intentpublisher,gapactivationstore}.go`,
|
||
`lobby/internal/adapters/userservice/`,
|
||
`lobby/internal/adapters/redisstate/gapactivationstore.go`,
|
||
`lobby/internal/adapters/{applicationstub,membershipstub,gapactivationstub,userservicestub,intentpubstub}/`,
|
||
the `Membership.canonical_key` field across
|
||
`lobby/internal/domain/membership/model.go` and
|
||
`lobby/internal/adapters/redisstate/codecs_membership.go`, the
|
||
`NewApplicationID`/`NewMembershipID` extensions to
|
||
`lobby/internal/adapters/idgen/`, and the new application routes wired
|
||
through `lobby/internal/api/{publichttp,internalhttp}/applications.go`.
|
||
|
||
Goal:
|
||
|
||
- implement the full public-game application lifecycle
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/ports/userservice.go`: `UserService` interface
|
||
(`GetEligibility(ctx, userID) (Eligibility, error)`).
|
||
- Add `internal/adapters/userservice/client.go`: HTTP client hitting
|
||
`GET /api/v1/internal/users/{user_id}/eligibility`.
|
||
- Add `internal/service/submitapplication/`:
|
||
- game type must be `public` and status `enrollment_open`
|
||
- call `UserService.GetEligibility`; fail if `can_join_game=false`
|
||
- call `RaceNameDirectory.Check(raceName, actorUserID)`; fail if name is taken
|
||
by another user (returns `name_taken`) or permanent-blocked
|
||
- create `Application{status: submitted, canonical_key}`
|
||
- publish `lobby.application.submitted` intent via `galaxy/notificationintent`
|
||
- Add `internal/service/approveapplication/`:
|
||
- call `RaceNameDirectory.Reserve(gameID, userID, raceName)`; idempotent
|
||
- create `Membership{status: active, canonical_key}`
|
||
- set application `status=approved`
|
||
- publish `lobby.membership.approved` intent
|
||
- trigger gap window open if `approved_count == max_players`
|
||
- Add `internal/service/rejectapplication/`:
|
||
- call `RaceNameDirectory.ReleaseReservation(gameID, userID, raceName)` —
|
||
safe no-op when no reservation exists for the pair
|
||
- set application `status=rejected`
|
||
- publish `lobby.membership.rejected` intent
|
||
- Wire routes.
|
||
- Add service tests with in-memory stores, stubbed `UserService`, and stub
|
||
`RaceNameDirectory`.
|
||
|
||
Exit criteria:
|
||
|
||
- all three application operations pass tests.
|
||
- eligibility denial surfaces as `eligibility_denied` error.
|
||
- name conflict surfaces as `name_taken` error.
|
||
- all three notifications are published in success paths.
|
||
|
||
## ~~Stage 12.~~ Invite Flow (Private Games)
|
||
|
||
Status: implemented — see `lobby/docs/stage12-invite-flow-decisions.md`,
|
||
`lobby/internal/service/{createinvite,redeeminvite,declineinvite,revokeinvite}`,
|
||
the new `NewInviteID` extension across
|
||
`lobby/internal/ports/idgenerator.go` and
|
||
`lobby/internal/adapters/idgen/`, the in-process
|
||
`lobby/internal/adapters/invitestub/` test adapter, and the four invite
|
||
routes wired through `lobby/internal/api/publichttp/invites.go`.
|
||
|
||
Goal:
|
||
|
||
- implement the full private-game invite lifecycle
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/createinvite/`:
|
||
- game type must be `private`, status `enrollment_open`
|
||
- invitee must not have an active invite or active membership in the game
|
||
- create `Invite{status: created, expires_at: game.enrollment_ends_at}`
|
||
- publish `lobby.invite.created` intent
|
||
- Add `internal/service/redeeminvite/`:
|
||
- invite status must be `created`, game status `enrollment_open`
|
||
- call `RaceNameDirectory.Check(raceName, actorUserID)`; fail if name is taken
|
||
by another user
|
||
- call `RaceNameDirectory.Reserve(gameID, userID, raceName)`
|
||
- create `Membership{status: active, canonical_key}`
|
||
- set invite `status=redeemed`
|
||
- publish `lobby.invite.redeemed` intent to owner
|
||
- trigger gap window open if `approved_count == max_players`
|
||
- Add `internal/service/declineinvite/`: set `status=declined`; no notification.
|
||
- Add `internal/service/revokeinvite/`: set `status=revoked`; no notification.
|
||
- Wire routes.
|
||
- Add service tests.
|
||
|
||
Exit criteria:
|
||
|
||
- redeem creates active membership without a separate approval step.
|
||
- race name is reserved atomically before membership creation.
|
||
- `lobby.invite.created` and `lobby.invite.redeemed` are published.
|
||
- decline and revoke produce no notification.
|
||
|
||
## ~~Stage 13.~~ Enrollment Automation Worker
|
||
|
||
Status: implemented — see `lobby/docs/stage13-enrollment-automation-decisions.md`,
|
||
`lobby/internal/worker/enrollmentautomation/`,
|
||
`lobby/internal/service/manualreadytostart/`,
|
||
`lobby/internal/service/shared/closeenrollment.go`, the
|
||
`GapActivationStore.Get` extension across
|
||
`lobby/internal/ports/gapactivationstore.go`,
|
||
`lobby/internal/adapters/redisstate/gapactivationstore.go`, and
|
||
`lobby/internal/adapters/gapactivationstub/store.go`, plus the
|
||
`POST /api/v1/lobby/games/{game_id}/ready-to-start` routes wired on both
|
||
ports through `lobby/internal/api/{publichttp,internalhttp}/ready_to_start.go`
|
||
and the worker registration in `lobby/internal/app/{wiring,runtime}.go`.
|
||
|
||
Goal:
|
||
|
||
- implement all automatic enrollment-to-ready-to-start transitions
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/worker/enrollmentautomation/worker.go`:
|
||
- periodic ticker with `LOBBY_ENROLLMENT_AUTOMATION_INTERVAL` (default `30s`)
|
||
- on each tick, load all games in `enrollment_open` status
|
||
- for each game check:
|
||
1. deadline: `now >= enrollment_ends_at && approved_count >= min_players`
|
||
2. gap exhaustion: gap window is open and (`now >= gap_activated_at + start_gap_hours`
|
||
or `approved_count >= max_players + start_gap_players`)
|
||
- on transition to `ready_to_start`:
|
||
- atomically expire all `created` invites for the game
|
||
- publish `lobby.invite.expired` intents (one per expired invite)
|
||
- Add `internal/service/manualreadytostart/`:
|
||
- admin/owner command
|
||
- require `approved_count >= min_players`
|
||
- same expiry and notification side effects as auto-transition
|
||
- Add gap window activation: when `approved_count` reaches `max_players`, record
|
||
`gap_activated_at` in Redis.
|
||
- Add tests using a fake clock; cover all three auto-transition paths and the
|
||
boundary condition where the deadline fires but `min_players` is not yet met.
|
||
|
||
Exit criteria:
|
||
|
||
- all three auto-transition paths are covered by tests.
|
||
- invite expiry on enrollment close is tested.
|
||
- the worker is idempotent: running twice over the same state produces no
|
||
duplicate transitions or notifications.
|
||
|
||
## ~~Stage 14.~~ Game Start Flow
|
||
|
||
Status: implemented — see `lobby/docs/stage14-game-start-flow-decisions.md`,
|
||
`lobby/internal/ports/{runtimemanager,gmclient,streamoffsetstore}.go`,
|
||
the `RuntimeBinding` field on `lobby/internal/domain/game/model.go` and
|
||
the new `GameStore.UpdateRuntimeBinding` port method,
|
||
`lobby/internal/adapters/{runtimemanager,gmclient,redisstate/streamoffsetstore.go,runtimemanagerstub,gmclientstub,streamoffsetstub}/`,
|
||
`lobby/internal/service/{startgame,retrystartgame}/`,
|
||
`lobby/internal/worker/runtimejobresult/`, the `LOBBY_RUNTIME_STOP_JOBS_STREAM`
|
||
env var, the public/internal `start` and `retry-start` HTTP routes, the
|
||
removal of the obsolete `register-runtime` endpoint from
|
||
`lobby/api/internal-openapi.yaml`, and the `runtime_binding` schema
|
||
addition on the `GameRecord` shape across both OpenAPI contracts.
|
||
|
||
Goal:
|
||
|
||
- implement the full start sequence spanning Runtime Manager and Game Master
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/ports/runtimemanager.go`: `RuntimeManager` interface
|
||
(`PublishStartJob(ctx, gameID string) error`,
|
||
`PublishStopJob(ctx, gameID string) error`).
|
||
- Add Redis stream adapter for `RuntimeManager` (write-only; publishes to
|
||
`runtime:start_jobs`).
|
||
- Add `internal/ports/gmclient.go`: `GMClient` interface
|
||
(`RegisterGame(ctx, req RegisterGameRequest) error`).
|
||
- Add `internal/adapters/gmclient/client.go`: HTTP client for GM registration.
|
||
- Add `internal/service/startgame/`:
|
||
- validate `ready_to_start`
|
||
- set status → `starting`
|
||
- publish start job to `RuntimeManager`
|
||
- Add `internal/worker/runtimejobresult/consumer.go`:
|
||
- consume `runtime:job_results` stream
|
||
- on failure result: set status → `start_failed`
|
||
- on success result:
|
||
- persist `runtime_binding` metadata on game record
|
||
- call `GMClient.RegisterGame` synchronously
|
||
- on GM success: set status → `running`; set `started_at`
|
||
- on GM failure/timeout: set status → `paused`; publish
|
||
`lobby.runtime_paused_after_start` intent
|
||
- on metadata persistence failure before GM call: publish stop job to
|
||
`RuntimeManager`; set status → `start_failed`
|
||
- Add `internal/service/retrystartgame/`: `start_failed → ready_to_start`.
|
||
- Wire consumer in `app/runtime.go`.
|
||
- Add tests with stubbed `RuntimeManager` and `GMClient`; cover all four
|
||
outcome paths.
|
||
|
||
Exit criteria:
|
||
|
||
- success path: game reaches `running` after container start and GM registration.
|
||
- paused path: GM unavailability produces `paused` + admin notification.
|
||
- failure path: container failure produces `start_failed`.
|
||
- orphan container path: metadata failure triggers stop job before `start_failed`.
|
||
- all paths covered by `go test ./... -race`.
|
||
|
||
## ~~Stage 14A.~~ Initial Player Stats Capture
|
||
|
||
Status: implemented — see `lobby/docs/stage14A-game-turn-stats-decisions.md`,
|
||
`lobby/internal/ports/gameturnstatsstore.go`,
|
||
`lobby/internal/adapters/redisstate/{gameturnstatsstore,codecs_gameturnstats}.go`,
|
||
and `lobby/internal/adapters/gameturnstatsstub/`.
|
||
|
||
Goal:
|
||
|
||
- freeze per-user `initial_planets` / `initial_population` at the first
|
||
`runtime_snapshot_update` after `starting → running`
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/ports/gameturnstatsstore.go`: `GameTurnStatsStore` with
|
||
`SaveInitial(ctx, gameID, stats []PlayerInitialStats) error`,
|
||
`UpdateMax(ctx, gameID, stats []PlayerObservedStats) error`,
|
||
`Load(ctx, gameID) (GameTurnStatsAggregate, error)`,
|
||
`Delete(ctx, gameID) error` (invoked after capability evaluation).
|
||
- Add `internal/adapters/redisstate/gameturnstatsstore.go` keyed under
|
||
`lobby:game_turn_stats:<game_id>:<user_id>`; tests with `miniredis`.
|
||
- Extend the GM event DTO in `internal/worker/gmevents/` to decode
|
||
`player_turn_stats`.
|
||
- In the consumer, invoke `SaveInitial` once per game (no-op on subsequent
|
||
calls to preserve the first observation) and `UpdateMax` on every
|
||
`runtime_snapshot_update`.
|
||
|
||
Exit criteria:
|
||
|
||
- Initial stats do not change on subsequent snapshots.
|
||
- `UpdateMax` uses per-metric max semantics (never decreases).
|
||
- Idempotent replay of the GM stream produces the same aggregate.
|
||
|
||
## ~~Stage 15.~~ GM Runtime Stream Consumer
|
||
|
||
Status: implemented — see `lobby/docs/stage15-gm-events-consumer-decisions.md`
|
||
and `lobby/internal/worker/gmevents/consumer.go`. The consumer wires the
|
||
existing `LOBBY_GM_EVENTS_STREAM` and `LOBBY_GM_EVENTS_READ_BLOCK_TIMEOUT`
|
||
configuration through `lobby/internal/app/{wiring,runtime}.go` and hands
|
||
off to the Stage 15A capability evaluator on `game_finished`.
|
||
|
||
Goal:
|
||
|
||
- keep the denormalized runtime snapshot current using GM events and feed
|
||
Stage 14A stats + Stage 15A capability evaluation
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/worker/gmevents/consumer.go`:
|
||
- consume `gm:lobby_events` stream
|
||
- on `runtime_snapshot_update` event:
|
||
- call `GameStore.UpdateRuntimeSnapshot` (turn, status, health)
|
||
- call `GameTurnStatsStore.SaveInitial` (first call only) and
|
||
`UpdateMax` using `player_turn_stats` (Stage 14A)
|
||
- on `game_finished` event:
|
||
- apply final snapshot; transition game to `finished`; set `finished_at`
|
||
- hand off to Stage 15A capability evaluator before acknowledging offset
|
||
- advance stream offset only after successful processing
|
||
- Add tests using `miniredis` with fake events; cover snapshot update,
|
||
game_finished, and replay idempotency.
|
||
|
||
Exit criteria:
|
||
|
||
- snapshot updates are applied without changing game status.
|
||
- `game_finished` transitions game to `finished`, sets `finished_at`, and
|
||
drives capability evaluation before offset advance.
|
||
- consumer restarts from the persisted offset without double-processing stats.
|
||
|
||
## ~~Stage 15A.~~ Capability Evaluation at Game Finish
|
||
|
||
Status: implemented — see `lobby/docs/stage15A-capability-evaluation-decisions.md`,
|
||
`lobby/internal/service/capabilityevaluation/service.go`,
|
||
`lobby/internal/ports/evaluationguardstore.go`,
|
||
`lobby/internal/adapters/redisstate/evaluationguardstore.go`, and
|
||
`lobby/internal/adapters/evaluationguardstub/`. Race-name notification
|
||
intents are wired through the `RaceNameIntents` port-shim and bound to
|
||
`capabilityevaluation.NoopRaceNameIntents{}` until Stage 24 lands the
|
||
real publisher.
|
||
|
||
Goal:
|
||
|
||
- decide per-member capability and resolve each active reservation into
|
||
`pending_registration` or immediate release when a game finishes
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/capabilityevaluation/service.go`:
|
||
- input: finished game id, final stats aggregate from `GameTurnStatsStore`
|
||
- for each active membership:
|
||
- `capable = max_planets > initial_planets AND max_population > initial_population`
|
||
- capable ⇒ `RND.MarkPendingRegistration(gameID, userID, raceName,
|
||
finished_at + 30 days)` + intent
|
||
`lobby.race_name.registration_eligible`
|
||
- not capable ⇒ `RND.ReleaseReservation(gameID, userID, raceName)` +
|
||
(optional) intent `lobby.race_name.registration_denied`
|
||
- for `removed` / `blocked` memberships with outstanding reservations:
|
||
release immediately
|
||
- delete `GameTurnStatsStore` aggregate for the game after evaluation
|
||
- Hook the evaluator into `gmevents` consumer after `game_finished` processing.
|
||
- Tests for capable / not-capable / mixed rosters, and for idempotency on
|
||
replay.
|
||
|
||
Exit criteria:
|
||
|
||
- every `active` membership of a finished game produces exactly one RND
|
||
side effect (mark pending or release).
|
||
- replayed `game_finished` events do not mutate RND state after the first
|
||
successful evaluation (idempotency guard keyed on game id).
|
||
- intents publish only after the RND mutation commits.
|
||
|
||
## ~~Stage 16.~~ Paused State Management
|
||
|
||
Status: implemented — see `lobby/docs/stage16-paused-state-decisions.md`,
|
||
`lobby/internal/service/{pausegame,resumegame}/`, the `Ping` extension
|
||
on `lobby/internal/ports/gmclient.go` together with its real
|
||
(`lobby/internal/adapters/gmclient/client.go`) and stub
|
||
(`lobby/internal/adapters/gmclientstub/client.go`) implementations,
|
||
and the public/internal pause/resume handlers wired through
|
||
`lobby/internal/api/{publichttp,internalhttp}/pause_resume.go`,
|
||
`lobby/internal/app/{wiring,runtime}.go`.
|
||
|
||
Goal:
|
||
|
||
- implement voluntary pause and resume
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/pausegame/`:
|
||
- actor must be admin or owner
|
||
- game must be `running`
|
||
- transition to `paused`
|
||
- Add `internal/service/resumegame/`:
|
||
- actor must be admin or owner
|
||
- game must be `paused`
|
||
- perform a synchronous GM liveness check (`GMClient.Ping` or equivalent)
|
||
- on GM reachable: transition to `running`
|
||
- on GM unreachable: return `service_unavailable`; game remains `paused`
|
||
- Wire routes.
|
||
- Add tests; cover GM-unreachable resume attempt.
|
||
|
||
Exit criteria:
|
||
|
||
- pause and resume operations enforce authorization and status invariants.
|
||
- resume does not transition to `running` when GM is unavailable.
|
||
|
||
## ~~Stage 17.~~ Member Operations
|
||
|
||
Status: implemented — see `lobby/docs/stage17-member-operations-decisions.md`,
|
||
`lobby/internal/service/{removemember,blockmember}`, the
|
||
`MembershipStore.Delete` extension across `lobby/internal/ports/membershipstore.go`,
|
||
`lobby/internal/adapters/redisstate/membershipstore.go`, and
|
||
`lobby/internal/adapters/membershipstub/store.go`, plus the public/internal
|
||
remove/block handlers wired through
|
||
`lobby/internal/api/{publichttp,internalhttp}/memberships.go` and
|
||
`lobby/internal/app/{wiring,runtime}.go`.
|
||
|
||
Goal:
|
||
|
||
- implement member removal and block
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/removemember/`:
|
||
- before game start: drop membership; call
|
||
`RND.ReleaseReservation(gameID, userID, raceName)`
|
||
- after game start: set membership `status=removed`; keep the reservation
|
||
intact so `game_finished` evaluation decides its fate (Stage 15A)
|
||
- Add `internal/service/blockmember/`:
|
||
- set membership `status=blocked`
|
||
- race name reservation is preserved; Stage 15A releases it at
|
||
`game_finished`
|
||
- Wire routes.
|
||
- Add tests; cover pre-start and post-start removal semantics, including the
|
||
interaction with Stage 15A for post-start remove/block.
|
||
|
||
Exit criteria:
|
||
|
||
- removal before start releases the reservation immediately.
|
||
- removal/block after start keeps the reservation until `game_finished` and
|
||
Stage 15A releases it.
|
||
|
||
## ~~Stage 17A.~~ Race Name Registration Service
|
||
|
||
Status: implemented — see `lobby/docs/stage17A-race-name-registration-decisions.md`,
|
||
`lobby/internal/service/registerracename/`,
|
||
`lobby/internal/api/publichttp/racenames.go`, the
|
||
`writeErrorFromService` extension in
|
||
`lobby/internal/api/publichttp/games.go` (with the new
|
||
`shared.ErrSubjectNotFound` sentinel), the public OpenAPI surface
|
||
update in `lobby/api/public-openapi.yaml`, and the wiring through
|
||
`lobby/internal/app/{wiring,runtime}.go`.
|
||
|
||
Goal:
|
||
|
||
- let a player convert a `pending_registration` reservation into a permanent
|
||
registered race name
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/registerracename/`:
|
||
- input: `{race_name, source_game_id}`; acting user from `X-User-ID`
|
||
- preconditions:
|
||
- canonical-key `pending_registration` exists for
|
||
`(source_game_id, user_id)` with `eligible_until > now`
|
||
- `UserService.GetEligibility` snapshot: active
|
||
`max_registered_race_names` > current registered count
|
||
(`0` denotes unlimited); `can_update_profile` is not required
|
||
- no `permanent_block` on the user
|
||
- commit: `RND.Register(source_game_id, user_id, race_name)`; emit intent
|
||
`lobby.race_name.registered`
|
||
- Wire route `POST /api/v1/lobby/race-names/register` on the public port.
|
||
- Tests: happy path, quota exceeded (`race_name_registration_quota_exceeded`),
|
||
pending expired (`race_name_pending_window_expired`), pending missing
|
||
(`subject_not_found`), permanent-blocked user (`forbidden`).
|
||
|
||
Exit criteria:
|
||
|
||
- Register call is atomic relative to RND reservation state.
|
||
- Quota logic matches the snapshot semantics (free=1, monthly=2, yearly=6,
|
||
lifetime=0 marker for unlimited).
|
||
- Intent emits only after successful commit.
|
||
|
||
## ~~Stage 17B.~~ Pending Registration Expiration Worker
|
||
|
||
Status: implemented — see `lobby/docs/stage17B-pending-registration-worker-decisions.md`,
|
||
`lobby/internal/worker/pendingregistration/`, the new
|
||
`PendingRegistrationConfig` group threaded through
|
||
`lobby/internal/config/{config,env,validation}.go`, and the worker
|
||
registration in `lobby/internal/app/{wiring,runtime}.go`.
|
||
|
||
Goal:
|
||
|
||
- release every `pending_registration` whose `eligible_until` has passed
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/worker/pendingregistration/worker.go`:
|
||
- ticker with `LOBBY_RACE_NAME_EXPIRATION_INTERVAL` (default `1h`)
|
||
- call `RND.ExpirePendingRegistrations(now)`
|
||
- for each expired entry: release the reservation and increment
|
||
`lobby.pending_registration.expirations`
|
||
- no notification (informational only)
|
||
- Tests using a fake clock and `miniredis`: boundary exactly at `eligible_until`,
|
||
batch of mixed-age entries, idempotent second tick.
|
||
|
||
Exit criteria:
|
||
|
||
- running the worker twice over the same state produces no extra side
|
||
effects.
|
||
- Stage 17A users who act before expiration still succeed (no race with the
|
||
worker).
|
||
|
||
## ~~Stage 17C.~~ Race Name Self-Service Reads
|
||
|
||
Status: implemented — see `lobby/docs/stage17C-race-name-self-service-decisions.md`,
|
||
`lobby/internal/service/listmyracenames/`, the new
|
||
`GET /api/v1/lobby/my/race-names` route on
|
||
`lobby/internal/api/publichttp/racenames.go` with the
|
||
`Dependencies.ListMyRaceNames` field on
|
||
`lobby/internal/api/publichttp/server.go`, the wiring through
|
||
`lobby/internal/app/{wiring,runtime}.go`, the `MyRaceNamesResponse`/
|
||
`PendingRaceName`/`RaceNameReservation` schemas added to
|
||
`lobby/api/public-openapi.yaml` (with the matching
|
||
`TestPublicSpecFreezesMyRaceNamesContract` in
|
||
`lobby/contract_openapi_test.go`), and the expanded
|
||
`lobby/README.md` §Race Name self-service section.
|
||
|
||
Goal:
|
||
|
||
- give the acting user a single view of their registered / pending / active
|
||
reservations
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/listmyracenames/`:
|
||
- returns `{registered[], pending[], reservations[]}`
|
||
- `pending` carries `eligible_until_ms` and `source_game_id`
|
||
- `reservations` carries `game_id` and current `game_status`
|
||
- Wire `GET /api/v1/lobby/my/race-names`; update `public-openapi.yaml`.
|
||
- Visibility test: a user cannot read another user's RND state through this
|
||
endpoint.
|
||
|
||
Exit criteria:
|
||
|
||
- response shape matches `lobby/README.md` §Race Name self-service.
|
||
- operation avoids scanning the full RND (uses
|
||
`user_registered` / `user_reservations` indexes).
|
||
|
||
## ~~Stage 18.~~ Query and Read APIs
|
||
|
||
Status: implemented — see `lobby/docs/stage18-query-and-read-apis-decisions.md`,
|
||
the six new service packages
|
||
`lobby/internal/service/{getgame,listgames,listmemberships,listmygames,
|
||
listmyapplications,listmyinvites}/`, the shared pagination helper
|
||
`lobby/internal/service/shared/page.go`, the public-port handlers in
|
||
`lobby/internal/api/publichttp/{games,memberships,mylists}.go`, the
|
||
internal-port handlers in
|
||
`lobby/internal/api/internalhttp/{games,memberships}.go`, and the
|
||
wiring updates in `lobby/internal/app/{wiring,runtime}.go`.
|
||
|
||
Goal:
|
||
|
||
- implement all user-facing list and read operations with visibility enforcement
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/service/getgame/`:
|
||
- enforce visibility rules: private game hidden from non-member non-owner users
|
||
- return runtime snapshot from denormalized fields
|
||
- Add `internal/service/listgames/`:
|
||
- public list: `enrollment_open`, `ready_to_start`, `running`, `finished` only
|
||
- authenticated user also sees their private game memberships
|
||
- Add `internal/service/listmemberships/`:
|
||
- admin, owner, or active member may list memberships of a game
|
||
- Wire `lobby.my_games.list`, `lobby.my_applications.list`, and
|
||
`lobby.my_invites.list` routes.
|
||
- Add tests for visibility rules.
|
||
|
||
Exit criteria:
|
||
|
||
- private game is not returned for non-member non-owner callers.
|
||
- public draft game is excluded from the public list.
|
||
- lists return correct entities for the authenticated user.
|
||
|
||
## ~~Stage 19.~~ Observability
|
||
|
||
Status: implemented — see `lobby/docs/stage19-observability-decisions.md`,
|
||
the extended `lobby/internal/telemetry/runtime.go` (15 instruments + 4
|
||
observable gauges + `RegisterGauges`), the new `IntentPublisher` /
|
||
`RaceNameDirectory` metric decorators in
|
||
`lobby/internal/adapters/{metricsintentpub,metricsracenamedir}/`, the
|
||
`GameStore.CountByStatus` extension across `lobby/internal/ports/gamestore.go`
|
||
and the redisstate / gamestub adapters, the new
|
||
`lobby/internal/ports/streamlagprobe.go` port with the redisstate adapter
|
||
and stub, the `httpcommon.RequestID` middleware wired on both HTTP
|
||
listeners, the `logging.ContextAttrs` helper plus the
|
||
`trace_id` / `span_id` rename in `logging/logger.go`, and the
|
||
service / worker threading of `*telemetry.Runtime` through every
|
||
status-transition / outcome / cascade success path.
|
||
|
||
Goal:
|
||
|
||
- instrument the service for operational support
|
||
|
||
Tasks:
|
||
|
||
- Add counters and gauges listed in `README.md` Observability section using the
|
||
OpenTelemetry SDK.
|
||
- Add structured log fields for all key operations (transitions, notification
|
||
publishes, enrollment automation triggers, stream consumer events).
|
||
- Propagate `request_id` and `trace_id` through all service calls and into
|
||
structured logs where available.
|
||
|
||
Exit criteria:
|
||
|
||
- process exports all listed metrics when a real or stdout OTEL exporter is
|
||
configured.
|
||
- key operations produce log entries with stable field names.
|
||
|
||
## ~~Stage 20.~~ Test Coverage and Documentation Alignment
|
||
|
||
Status: implemented — see
|
||
`lobby/docs/stage20-test-coverage-and-doc-alignment-decisions.md`,
|
||
the new `integration/lobbyuser/` package (4 boundary tests against the
|
||
real `user/cmd/userservice` binary), the new `integration/lobbynotification/`
|
||
package (4 scenario tests covering 8 of 11 lobby `notification:intents`
|
||
producer types), the updated `LOBBY_PENDING_REGISTRATION_TTL_HOURS`
|
||
clarification at `lobby/README.md:1130-1136`, the
|
||
`/healthz`/`/readyz` realignment in
|
||
`lobby/api/internal-openapi.yaml:49,66`, and the new
|
||
`TestPublicSpecDeclaresAllRegisteredRoutes` /
|
||
`TestInternalSpecDeclaresAllRegisteredRoutes` route-table contract tests
|
||
in `lobby/contract_openapi_test.go`. `ARCHITECTURE.md` §7 and §10 were
|
||
spot-checked and required no edits.
|
||
|
||
Goal:
|
||
|
||
- close the loop across service tests, boundary tests, and documentation
|
||
|
||
Tasks:
|
||
|
||
- Verify all `README.md` claims against the implemented behavior.
|
||
- Add integration tests in the `integration/` module for:
|
||
- `Lobby → User Service` eligibility check boundary
|
||
- `Lobby → Notification Service` intent publication for all seven types
|
||
- Align `lobby/api/public-openapi.yaml` and `internal-openapi.yaml` with the
|
||
final implemented routes.
|
||
- Run `go test ./... -race -cover` across the lobby module.
|
||
- Verify `ARCHITECTURE.md` still matches the final implementation.
|
||
|
||
Exit criteria:
|
||
|
||
- `go test ./... -race` passes for the lobby module and the integration module.
|
||
- no contradictions between `lobby/README.md`, `ARCHITECTURE.md`, and implemented
|
||
behavior.
|
||
|
||
## ~~Stage 21.~~ User Service: `user_name` + `display_name` refactor
|
||
|
||
Status: implemented — see `user/docs/stage21-user-name-display-name.md`,
|
||
`lobby/internal/domain/racename/`, and the Gateway boundary rename across
|
||
`pkg/schema/fbs/user.fbs`, `pkg/transcoder/user.go`, `pkg/model/user`, and
|
||
the integration + gateway contract tests.
|
||
|
||
Cross-service stage owned by `galaxy/user`. Must land before Stage 17A so the
|
||
eligibility snapshot carries `max_registered_race_names`. Can run in parallel
|
||
with Stages 09R and 10 once Stage 21.1–21.4 are complete.
|
||
|
||
Tasks:
|
||
|
||
- 21.1. Add `UserName` and `DisplayName` value types in
|
||
`user/internal/domain/common/types.go` (or an adjacent file). `UserName`
|
||
matches `player-<suffix>` with suffix 8 characters from a confusable-free
|
||
alphanumeric alphabet; `DisplayName` delegates validation to
|
||
`pkg/util/string.go:ValidateTypeName` and tolerates empty strings.
|
||
- 21.2. Replace `RaceName` with `UserName` and add `DisplayName` on
|
||
`UserAccount` in `user/internal/domain/account/model.go`. Delete
|
||
`RaceNameReservation` and `RaceNameCanonicalKey` types.
|
||
- 21.3. Rename `IDGenerator.NewInitialRaceName` → `NewUserName`. Update its
|
||
implementation to use an 8-character confusable-free alphanumeric suffix
|
||
(`AppendRandomSuffix` pattern in `pkg/util/string.go` is a reference but
|
||
will need a new alphabet). Keep collision retries by store response;
|
||
increase the `ensureCreateRetryLimit` from `8` to `10`.
|
||
- 21.4. Delete `user/internal/ports/race_name_policy.go` and its adapters.
|
||
Move confusable-pair policy (including fixtures and tests) to
|
||
`lobby/internal/domain/racename/` — this feeds Stage 09R.
|
||
- 21.5. Update `authdirectory.Ensurer`: the ensure-by-email path creates
|
||
`UserName` via the renamed generator; `DisplayName` remains empty; no race
|
||
name reservation is created.
|
||
- 21.6. Update `selfservice.ProfileUpdater`: accept only `display_name`,
|
||
validate via `ValidateTypeName`. `user_name` is immutable and returned
|
||
read-only in the account view.
|
||
- 21.7. Extend `lobbyeligibility.SnapshotReader` to materialize
|
||
`max_registered_race_names` in `EffectiveLimits` (free=1, paid_monthly=2,
|
||
paid_yearly=6, paid_lifetime=0 marker) and to respect any user-specific
|
||
`LimitCodeMaxRegisteredRaceNames` override.
|
||
- 21.8. Extend `adminusers` list/search: exact + prefix filters by
|
||
`user_name` and `display_name`; update listing ordering if needed.
|
||
- 21.9. Update `user/internal/api/internalhttp/` handlers, `user/openapi.yaml`,
|
||
and contract tests (`openapi_contract_test.go`, `runtime_contract_test.go`).
|
||
- 21.10. Update `user/README.md` and `user/docs/` to reflect
|
||
`user_name`/`display_name`. Remove every reference to `race_name` in user
|
||
docs.
|
||
- 21.11. Update `integration/` cross-service tests (gateway scenarios,
|
||
auth/session wiring, lobby eligibility consumption).
|
||
|
||
Exit criteria:
|
||
|
||
- `go test ./... -race` passes for the user module and integration module.
|
||
- ensure-by-email returns only `user_id`, populating `user_name` and leaving
|
||
`display_name` empty.
|
||
- update-my-profile modifies only `display_name`.
|
||
- eligibility snapshot JSON carries `max_registered_race_names`.
|
||
- no source file in `galaxy/user` references `race_name` or
|
||
`RaceNameReservation` after the stage.
|
||
|
||
## ~~Stage 22~~ — User Service: `permanent_block` + `DeleteUser`
|
||
|
||
Cross-service stage owned by `galaxy/user`. Required before Stage 23.
|
||
|
||
Tasks:
|
||
|
||
- 22.1. Add `policy.SanctionCodePermanentBlock` to the supported catalog;
|
||
extend lobby-relevant filter so that the sanction always surfaces in the
|
||
eligibility snapshot; update `deriveEligibilityMarkers` so that an active
|
||
`permanent_block` collapses every `can_*` marker to `false`.
|
||
- 22.2. Add `policy.LimitCodeMaxRegisteredRaceNames` to the supported catalog
|
||
so admin overrides are possible.
|
||
- 22.3. Add `service/accountdeletion/` (new) and
|
||
`POST /api/v1/internal/users/{user_id}/delete` endpoint.
|
||
Soft-delete: mark `UserAccount.DeletedAt`; reject all subsequent auth,
|
||
self-service, admin-read, and lobby-eligibility operations with
|
||
`subject_not_found` for external callers; emit `user.lifecycle.deleted`
|
||
event.
|
||
- 22.4. Add `ports.UserLifecyclePublisher` and Redis stream
|
||
`user:lifecycle_events`. Emit:
|
||
- `user.lifecycle.permanent_blocked` on application of
|
||
`SanctionCodePermanentBlock` via `adminusers` path;
|
||
- `user.lifecycle.deleted` on successful `DeleteUser`.
|
||
Fields: `user_id`, `occurred_at_ms`, `actor`, `reason_code`.
|
||
- 22.5. Update `user/openapi.yaml`, handlers, and contract tests.
|
||
|
||
Exit criteria:
|
||
|
||
- permanent_block surfaces in the eligibility snapshot and drives all can_*
|
||
to false.
|
||
- DeleteUser is idempotent per `user_id`; a second call after soft-delete
|
||
returns `subject_not_found`.
|
||
- `user:lifecycle_events` receives exactly one event per state transition.
|
||
|
||
## ~~Stage 23.~~ Lobby: `user:lifecycle_events` consumer + cascade release
|
||
|
||
Status: implemented — see `lobby/docs/stage23-user-lifecycle-consumer-decisions.md`,
|
||
the new `lobby/internal/ports/userlifecyclestream.go`,
|
||
`lobby/internal/adapters/userlifecycle/consumer.go` (with the in-memory
|
||
`lobby/internal/adapters/userlifecyclestub/consumer.go` for tests),
|
||
`lobby/internal/worker/userlifecycle/worker.go`, the
|
||
`InviteStore.GetByInviter` and `GameStore.GetByOwner` extensions across
|
||
ports + redisstate + stubs, the new `game.TriggerExternalBlock` plus
|
||
`*/in-flight → cancelled` transitions in
|
||
`lobby/internal/domain/game/status.go`, the synchronous
|
||
`UserService.GetEligibility` guard added to
|
||
`lobby/internal/service/redeeminvite/service.go`, the `LOBBY_USER_LIFECYCLE_*`
|
||
configuration knobs in `lobby/internal/config/{config,env}.go`, the
|
||
worker + consumer wiring in `lobby/internal/app/{wiring,runtime}.go`, the
|
||
new `lobby.membership.blocked` notification type across
|
||
`pkg/notificationintent/{intent,payloads,intent_test}.go`,
|
||
`pkg/schema/fbs/notification.fbs` (with regenerated
|
||
`pkg/schema/fbs/notification/LobbyMembershipBlockedEvent.go`),
|
||
`pkg/transcoder/notification.go`,
|
||
`notification/api/intents-asyncapi.yaml`,
|
||
`notification/internal/{api/intentstream/contract,service/publishpush/encoder}.go`,
|
||
the contract-test fixtures in
|
||
`notification/{contract_asyncapi,producer_integration_contract,push_payload_contract,mail_template_contract}_test.go`,
|
||
the new mail templates under
|
||
`mail/templates/lobby.membership.blocked/en/`, and the README updates in
|
||
`lobby/README.md` (Notification Contracts, Cascade release, Status
|
||
transition table, Redis Logical Model, Observability) plus
|
||
`notification/README.md` and `gateway/README.md`.
|
||
|
||
Tasks:
|
||
|
||
- Add `internal/ports/userlifecyclestream.go`: `UserLifecycleConsumer`
|
||
abstraction with `Run(ctx) error` and `OnEvent(handler)`.
|
||
- Add `internal/adapters/userlifecycle/consumer.go`: Redis Streams consumer;
|
||
offset persisted at `lobby:stream_offsets:user_lifecycle`.
|
||
- Add `internal/worker/userlifecycle/worker.go`:
|
||
- on `user.lifecycle.permanent_blocked` or `user.lifecycle.deleted`:
|
||
- `RND.ReleaseAllByUser(user_id)`;
|
||
- mark every active `Membership` for the user as `blocked` with trigger
|
||
`external_block`;
|
||
- cancel every `submitted` application and every `created` invite owned or
|
||
addressed to the user;
|
||
- publish `lobby.membership.blocked` intents to private game owners where
|
||
applicable (reuse existing notification type or introduce
|
||
`lobby.user.permanent_blocked` — freeze choice in Stage 03R).
|
||
- Wire worker startup in `app/runtime.go`.
|
||
- Tests (`miniredis` + fake stream): full cascade, replay idempotency, partial
|
||
failure retry.
|
||
|
||
Exit criteria:
|
||
|
||
- A `permanent_blocked` event releases every RND entry for the user and
|
||
settles every lobby artefact atomically (per-entity operations OK; overall
|
||
consistency is eventual but within one event pass).
|
||
- Replaying the stream does not double-release.
|
||
- Offset advances only after full event handling.
|
||
|
||
## ~~Stage 24.~~ Notification intent catalog additions
|
||
|
||
Status: implemented — see `lobby/docs/stage24-race-name-intents-decisions.md`,
|
||
the new constants and constructors in
|
||
`pkg/notificationintent/{intent,payloads}.go` (with corresponding test
|
||
rows in `intent_test.go`),
|
||
the AsyncAPI extension in `notification/api/intents-asyncapi.yaml` and
|
||
contract-test fixture updates across
|
||
`notification/{contract_asyncapi,producer_integration_contract,mail_template_contract,push_payload_contract}_test.go`,
|
||
the new `LobbyRaceNameRegistrationEligibleEvent` and
|
||
`LobbyRaceNameRegisteredEvent` tables in
|
||
`pkg/schema/fbs/notification.fbs` with regenerated Go bindings,
|
||
the matching transcoder helpers in
|
||
`pkg/transcoder/notification.go`, the new switch arms in
|
||
`notification/internal/service/publishpush/encoder.go`, mail templates
|
||
under `mail/templates/lobby.race_name.{registration_eligible,registered,registration_denied}/en/`,
|
||
the gateway README push vocabulary update, the new
|
||
`lobby/internal/adapters/racenameintents/` adapter wired through
|
||
`lobby/internal/app/wiring.go` (replacing
|
||
`capabilityevaluation.NoopRaceNameIntents{}`), and the
|
||
`notification/internal/api/intentstream/contract.go` aliases.
|
||
|
||
Tasks:
|
||
|
||
- Extend `pkg/notificationintent/intent.go` vocabulary with
|
||
`NotificationTypeLobbyRaceNameRegistrationEligible`,
|
||
`NotificationTypeLobbyRaceNameRegistered`, and (optional)
|
||
`NotificationTypeLobbyRaceNameRegistrationDenied`.
|
||
- Update `ExpectedProducer`, `SupportsAudience`, `SupportsChannel`, and
|
||
`validatePayloadObject` accordingly.
|
||
- Extend `pkg/notificationintent/payloads.go` with:
|
||
- `LobbyRaceNameRegistrationEligiblePayload{GameID, GameName, RaceName, EligibleUntilMs}`
|
||
- `LobbyRaceNameRegisteredPayload{RaceName}`
|
||
- `LobbyRaceNameRegistrationDeniedPayload{GameID, GameName, RaceName, Reason}`
|
||
and matching `NewXxxIntent` constructors.
|
||
- Update `notification/api/intents-asyncapi.yaml` and related contract tests.
|
||
|
||
Exit criteria:
|
||
|
||
- `pkg/notificationintent` tests cover all new types end-to-end (build,
|
||
encode, decode, validate).
|
||
- AsyncAPI contract stays valid.
|
||
- Stage 15A and Stage 17A can publish intents via the new constructors.
|
||
|
||
## Execution Order
|
||
|
||
1. ~~Stage 21~~ — User Service refactor.
|
||
2. ~~Stage 22~~ — User Service `permanent_block` + `DeleteUser`.
|
||
3. ~~Stage 01R, 03R~~ — documentation alignment.
|
||
4. ~~Stage 09R~~ — RND port + Redis adapter.
|
||
5. ~~Stage 10.~~
|
||
6. ~~Stage 11, 12~~ — updated race name flows.
|
||
7. ~~Stage 13~~, ~~14~~, ~~14A~~.
|
||
8. ~~Stage 15~~, ~~15A~~.
|
||
9. ~~Stage 16.~~
|
||
10. ~~Stage 17.~~
|
||
11. ~~Stage 24~~ — notification catalog (prerequisite for 15A/17A intents).
|
||
12. Stage ~~17A~~, ~~17B~~, ~~17C~~.
|
||
13. ~~Stage 18.~~
|
||
14. ~~Stage 23~~ — user lifecycle consumer.
|
||
15. ~~Stage 19.~~
|
||
16. ~~Stage 20.~~
|
||
|
||
## Final Acceptance Criteria
|
||
|
||
The implementation is complete only when all of the following hold:
|
||
|
||
- all status transition invariants are enforced; no unsupported transition can
|
||
be triggered by any API call
|
||
- enrollment automation handles all three auto-transition paths correctly and
|
||
idempotently
|
||
- Race Name Directory (two-tier) enforces platform-wide uniqueness across
|
||
registered names, active reservations, and pending registrations; canonical
|
||
key + confusable-pair policy applies to every path
|
||
- per-game reservations respect the "same user may hold the same name across
|
||
multiple active games" invariant
|
||
- initial `planets` / `population` are captured once per member per game from
|
||
the first post-start `runtime_snapshot_update`
|
||
- capability evaluation at `game_finished` resolves every active reservation
|
||
(pending if capable, released otherwise) atomically relative to the event
|
||
offset advance
|
||
- race name registration respects the tariff snapshot
|
||
(`max_registered_race_names`), the 30-day pending window, and idempotent
|
||
retry
|
||
- pending-registration expiration worker releases every expired entry without
|
||
double-releasing on restart
|
||
- `user:lifecycle_events` consumer cascades `permanent_blocked` and `deleted`
|
||
to full RND release, membership blocking, and application/invite
|
||
cancellation
|
||
- application and invite flows produce the correct notifications at each step
|
||
- game start flow handles runtime failure, GM unavailability, and metadata
|
||
persistence failure correctly
|
||
- GM runtime snapshot updates are applied durably from the stream and feed
|
||
the per-game stats aggregate
|
||
- game finish triggered by GM stream transitions the game correctly and
|
||
drives capability evaluation before offset advance
|
||
- denormalized runtime snapshot is always returned from the game record without
|
||
a round-trip to `Game Master`
|
||
- private game visibility rules are enforced at every list and read endpoint
|
||
- all configuration can be supplied via environment variables with documented
|
||
defaults
|
||
- User Service no longer owns a `race_name` concept; `user_name` and
|
||
`display_name` fully replace it, and ensure-by-email uses the renamed
|
||
generator
|
||
- `go test ./... -race` passes for the lobby module, the user module, the
|
||
`pkg/notificationintent` module, and the integration module
|
||
|
||
## Note: Runtime Manager Envelope Evolution
|
||
|
||
Subsequent changes to the `runtime:start_jobs` and `runtime:stop_jobs`
|
||
envelopes — specifically the addition of `image_ref` to the start envelope
|
||
and the addition of the `reason` enum to the stop envelope — are owned by
|
||
the Runtime Manager implementation plan, not by this document. See
|
||
[`../rtmanager/PLAN.md`](../rtmanager/PLAN.md) §«Stage 06. Lobby publisher
|
||
refactor». No new stages are added here for that work.
|
||
|
||
## Note: Game Master Refactor (image-ref + membership invalidate)
|
||
|
||
The retirement of `LOBBY_ENGINE_IMAGE_TEMPLATE` together with the
|
||
inline `engineimage.Resolver` package, the synchronous switch to
|
||
`Game Master`'s `GET /api/v1/internal/engine-versions/{version}/image-ref`
|
||
for image-ref resolution, and the new outgoing
|
||
`POST /api/v1/internal/games/{game_id}/memberships/invalidate` hook from
|
||
`approveapplication`, `rejectapplication`, `redeeminvite`,
|
||
`removemember`, `blockmember`, and the user-lifecycle cascade worker
|
||
are owned by the Game Master implementation plan, not by this document.
|
||
See [`../gamemaster/PLAN.md`](../gamemaster/PLAN.md) §«Stage 20. Lobby
|
||
refactor». No new stages are added here for that work.
|