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:
+39
-41
@@ -1,24 +1,24 @@
|
||||
# backend
|
||||
|
||||
Internal-only domain service for the Scrabble platform (module `scrabble/backend`).
|
||||
It owns identity/sessions, accounts, and — in later stages — the lobby, game
|
||||
runtime, robot, chat, history and administration. Its only network consumers are
|
||||
the `gateway` and the platform side-services; it is never exposed publicly.
|
||||
It owns identity/sessions, accounts, the lobby, game runtime, robot, chat, history
|
||||
and administration. Its only network consumers are the `gateway` and the platform
|
||||
side-services; it is never exposed publicly.
|
||||
|
||||
As of Stage 1 the backend provides the foundation: configuration, the HTTP
|
||||
listener with the `/api/v1` route-group skeleton and probes, the Postgres pool
|
||||
with embedded goose migrations, OpenTelemetry wiring, an in-memory session cache,
|
||||
and the durable accounts / identities / sessions data model. The session and
|
||||
account REST endpoints are added with the `gateway` (Stage 6); Stage 1 ships the
|
||||
store/service layer they will call.
|
||||
The backend provides the foundation: configuration, the HTTP listener with the
|
||||
`/api/v1` route-group skeleton and probes, the Postgres pool with embedded goose
|
||||
migrations, OpenTelemetry wiring, an in-memory session cache, and the durable
|
||||
accounts / identities / sessions data model. The session and account REST
|
||||
endpoints live in the `gateway`; the backend ships the store/service layer they
|
||||
call.
|
||||
|
||||
Stage 2 adds `internal/engine`, the in-process bridge to the `scrabble-solver`
|
||||
`internal/engine` is the in-process bridge to the `scrabble-solver`
|
||||
library: a versioned dictionary registry, a deterministic tile bag, and a pure
|
||||
rules `Game` (legal plays, passes, exchanges, resignations and end-condition
|
||||
detection) that emits dictionary-independent move records. It is a library only;
|
||||
the game domain wires it into the process in Stage 3.
|
||||
the game domain wires it into the process.
|
||||
|
||||
Stage 3 adds `internal/game`, the game domain over the engine. Active games are
|
||||
`internal/game` is the game domain over the engine. Active games are
|
||||
event-sourced: a `games` row plus an append-only decoded move journal, with the
|
||||
live `engine.Game` kept warm in a cache and rebuilt by replay on a miss. It
|
||||
provides create, the play/pass/exchange/resign transitions, an unlimited
|
||||
@@ -26,10 +26,9 @@ score/legality preview, the hint (per-game allowance plus a profile wallet), the
|
||||
word-check tool with complaint capture, per-player game state, history and GCG
|
||||
export, per-account statistics on finish, and a background turn-timeout sweeper
|
||||
that auto-resigns overdue turns (honouring each player's daily away window). Like
|
||||
Stages 1–2 it is a service/store layer; the HTTP surface lands with the
|
||||
`gateway` (Stage 6).
|
||||
the engine it is a service/store layer; the HTTP surface lives in the `gateway`.
|
||||
|
||||
Stage 4 adds the lobby and social fabric. `internal/lobby` holds an in-memory
|
||||
The lobby and social fabric. `internal/lobby` holds an in-memory
|
||||
matchmaking pool (FIFO per variant, pairs two humans into an auto-match) and
|
||||
friend-game invitations (invite → accept, starting a 2–4 player game once every
|
||||
invitee accepts). `internal/social` owns the friend graph (request/accept),
|
||||
@@ -41,10 +40,10 @@ development log mailer). The engine now also handles **multi-player drop-out**:
|
||||
a 3–4 player game a resignation or timeout drops that seat and the rest play on
|
||||
(the tile disposition is a per-game setting), the game ending when one active seat
|
||||
remains. As before this is a service/store layer — chat and nudges are persisted
|
||||
but their live delivery, and all REST endpoints, arrive with the `gateway`
|
||||
(Stage 6); the services are exposed via `Server` accessors for those handlers.
|
||||
but their live delivery, and all REST endpoints, live in the `gateway`; the
|
||||
services are exposed via `Server` accessors for those handlers.
|
||||
|
||||
Stage 5 adds the robot opponent (`internal/robot`). A pool of durable accounts —
|
||||
The robot opponent (`internal/robot`). A pool of durable accounts —
|
||||
each a `kind='robot'` identity, provisioned at startup with chat and friend
|
||||
requests blocked — backs human-like, per-language composed names. A background driver plays the
|
||||
robot's moves through the public game API as an ordinary seated player (so only
|
||||
@@ -53,28 +52,28 @@ win (≈ 40%), targets a small score margin, and times its moves with a move-num
|
||||
right-skewed delay (quick openings, long endgames), a night-sleep window anchored to the opponent's timezone, and nudge
|
||||
behaviour — all derived deterministically from the game seed, so it keeps no extra
|
||||
state. The matchmaker now substitutes a pooled robot (matching the game's language) after a 10-second wait and
|
||||
exposes `Poll` so a waiting player can collect the started game — R4 made it the **stream-down
|
||||
exposes `Poll` so a waiting player can collect the started game — it is the **stream-down
|
||||
fallback**: while the client is streaming, the live match-found push (enriched with the recipient's
|
||||
initial game state) drives it instead.
|
||||
|
||||
Stage 6 opens the backend to the edge. The route groups gain their first
|
||||
The backend opens to the edge. The route groups gain their first
|
||||
handlers (`internal/server/handlers_*.go`): gateway-only session endpoints under
|
||||
`/api/v1/internal` (Telegram/guest/email login → mint, resolve, revoke) and a
|
||||
slice of authenticated `/api/v1/user` operations (profile, submit play, game
|
||||
state, lobby enqueue/poll, chat). Stage 8 fills in the social/account/history
|
||||
operations under `/api/v1/user`: `friends/*` (request/respond/cancel/unfriend,
|
||||
state, lobby enqueue/poll, chat). The social/account/history operations under
|
||||
`/api/v1/user`: `friends/*` (request/respond/cancel/unfriend,
|
||||
list/incoming, the one-time `code` issue/redeem), `blocks/*`, `invitations/*`
|
||||
(create/accept/decline/cancel/list), `PUT profile`, `email/{request,confirm}`,
|
||||
`stats`, and `games/:id/gcg` (finished-only). A new `internal/notify` hub feeds a
|
||||
`stats`, and `games/:id/gcg` (finished-only). The `internal/notify` hub feeds a
|
||||
second listener — `internal/pushgrpc`, a gRPC server (`BACKEND_GRPC_ADDR`) streaming
|
||||
live events (your-turn, opponent-moved, chat, nudge, match-found, notify) to the
|
||||
gateway. Stage 9 adds the gateway-only `POST /api/v1/internal/push-target` (a user's
|
||||
Telegram `external_id`, language and `notifications_in_app_only` flag) that the gateway
|
||||
uses to route out-of-app push to the Telegram connector, extends the Telegram login to
|
||||
seed a new account's language and display name from the launch fields, and adds
|
||||
the `accounts.notifications_in_app_only` flag (default true).
|
||||
gateway. The gateway-only `POST /api/v1/internal/push-target` (a user's
|
||||
Telegram `external_id`, language and `notifications_in_app_only` flag) lets the gateway
|
||||
route out-of-app push to the Telegram connector; the Telegram login
|
||||
seeds a new account's language and display name from the launch fields, and the
|
||||
`accounts.notifications_in_app_only` flag (default true).
|
||||
`accounts.is_guest` marks an ephemeral guest — a durable row
|
||||
with no identity, excluded from statistics. **Stage 10** adds the server-rendered
|
||||
with no identity, excluded from statistics. The server-rendered
|
||||
**admin console** at `/_gm` (`internal/adminconsole` + `internal/server/handlers_admin_console.go`;
|
||||
the gateway fronts it with Basic-Auth and a same-origin guard protects its POSTs), the
|
||||
**complaint resolution** lifecycle (the `complaints` `disposition`/`resolution_note`/
|
||||
@@ -82,13 +81,13 @@ the gateway fronts it with Basic-Auth and a same-origin guard protects its POSTs
|
||||
pipeline, dictionary **hot-reload** from `BACKEND_DICT_DIR/<version>/`
|
||||
(`engine.OpenWithVersions` / `Registry.LoadAvailable`), and operator **broadcasts** via a
|
||||
backend Telegram-connector client (`internal/connector`, `BACKEND_CONNECTOR_ADDR`) — each
|
||||
broadcast picks the delivering bot by an operator-chosen language. **Stage 15** adds
|
||||
`accounts.service_language`: the language tag of the bot a Telegram
|
||||
broadcast picks the delivering bot by an operator-chosen language. `accounts.service_language`
|
||||
holds the language tag of the bot a Telegram
|
||||
user last signed in through, written on every login and returned by
|
||||
`/internal/push-target` (falling back to `preferred_language`) so out-of-app push routes
|
||||
to the right bot. The shared wire contracts live in the sibling [`../pkg`](../pkg) module.
|
||||
|
||||
Stage 11 adds **account linking & merge** (`/api/v1/user/link/*`). `internal/link`
|
||||
**Account linking & merge** (`/api/v1/user/link/*`). `internal/link`
|
||||
orchestrates it: an email confirm-code or a gateway-validated Telegram identity is
|
||||
attached to the current account, and when the identity already has its own account
|
||||
the two are merged in one transaction (`internal/accountmerge`) — stats and the hint
|
||||
@@ -98,9 +97,9 @@ shared finished game's foreign keys hold); a shared **active** game blocks the m
|
||||
The current account is primary, except a guest initiator whose linked identity has a
|
||||
durable owner — then the durable account wins and a fresh session is minted for it.
|
||||
The `accounts.paid_account`/`merged_into`/`merged_at` columns back this. This supersedes the
|
||||
Stage 8 `email.bind.*` edge surface (the `RequestCode`/`ConfirmCode` primitives stay).
|
||||
former `email.bind.*` edge surface (the `RequestCode`/`ConfirmCode` primitives stay).
|
||||
|
||||
**R3** adds rate-limit observability: the gateway posts its periodic rejection
|
||||
Rate-limit observability: the gateway posts its periodic rejection
|
||||
summaries to `POST /api/v1/internal/ratelimit/report`; `internal/ratewatch` keeps a
|
||||
bounded in-memory episode window for the console's **Throttled** page and applies the
|
||||
conservative auto-flag — an account sustaining `BACKEND_HIGHRATE_FLAG_THRESHOLD`
|
||||
@@ -119,8 +118,8 @@ internal/postgres/ # pgx-over-database/sql pool (otelsql), goose migrations
|
||||
migrations/ # embedded *.sql (goose), schema `backend`
|
||||
jet/ # generated go-jet models + table builders (committed)
|
||||
internal/account/ # durable accounts + platform/email identities (store) + email/identity link primitives
|
||||
internal/accountmerge/ # single-transaction merge of a secondary account into a primary (Stage 11)
|
||||
internal/link/ # link/merge orchestrator over account + accountmerge + session (Stage 11)
|
||||
internal/accountmerge/ # single-transaction merge of a secondary account into a primary
|
||||
internal/link/ # link/merge orchestrator over account + accountmerge + session
|
||||
internal/session/ # opaque tokens, sessions store, write-through cache, service (incl. RevokeAllForAccount)
|
||||
internal/server/ # gin engine, route groups, X-User-ID middleware, probes
|
||||
internal/engine/ # in-process scrabble-solver bridge: registry, bag, Game, replay
|
||||
@@ -130,7 +129,7 @@ internal/lobby/ # in-memory matchmaking pool (+ robot substitution) + frien
|
||||
internal/robot/ # human-like robot opponent: account pool, seed-derived strategy, move driver
|
||||
internal/adminconsole/ # server-rendered admin console (Go templates + embedded CSS, view models), served at /_gm
|
||||
internal/connector/ # backend gRPC client to the Telegram connector (operator broadcasts)
|
||||
internal/ratewatch/ # gateway rate-limit reports: episode window for the console + the high-rate auto-flag (R3)
|
||||
internal/ratewatch/ # gateway rate-limit reports: episode window for the console + the high-rate auto-flag
|
||||
```
|
||||
|
||||
## Configuration (environment)
|
||||
@@ -163,7 +162,7 @@ internal/ratewatch/ # gateway rate-limit reports: episode window for the consol
|
||||
| `BACKEND_CONNECTOR_ADDR` | — | Telegram connector gRPC address for admin-console operator broadcasts. Empty disables broadcasts. |
|
||||
| `BACKEND_GUEST_REAP_INTERVAL` | `1h` | How often the abandoned-guest reaper sweeps. |
|
||||
| `BACKEND_GUEST_RETENTION` | `720h` | Account age past which a guest with no game seat is deleted. |
|
||||
| `BACKEND_HIGHRATE_FLAG_THRESHOLD` | `1000` | Gateway-reported rejected calls within the window past which an account is soft-flagged (R3). |
|
||||
| `BACKEND_HIGHRATE_FLAG_THRESHOLD` | `1000` | Gateway-reported rejected calls within the window past which an account is soft-flagged. |
|
||||
| `BACKEND_HIGHRATE_FLAG_WINDOW` | `10m` | The rolling window those rejections accumulate over. |
|
||||
|
||||
## Run
|
||||
@@ -209,9 +208,8 @@ local solver co-development you may add a temporary replace — see `go.work`).
|
||||
(`en_sowpods.dawg`, `ru_scrabble.dawg`, `ru_erudit.dawg`) ship as a **release artifact**
|
||||
from the [`scrabble-dictionary`](https://gitea.iliadenisov.ru/developer/scrabble-dictionary)
|
||||
repo (one semver per set); the engine loads them by `(variant, dict_version)` from
|
||||
`BACKEND_DICT_DIR`. Since Stage 3 the backend loads them at startup as a hard dependency
|
||||
(a missing dictionary aborts the boot). See [`../PLAN.md`](../PLAN.md) Stage 14
|
||||
(TODO-1/TODO-2).
|
||||
`BACKEND_DICT_DIR`. The backend loads them at startup as a hard dependency
|
||||
(a missing dictionary aborts the boot).
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
Reference in New Issue
Block a user