Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod data; verified schema-identical to the chain via a pg_dump diff + the green integration suite) and rename the game-variant labels english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the backend, the FlatBuffers wire values and the UI. dawg filenames and the Go enum identifiers are unchanged; the i18n display keys are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups.
8.3 KiB
scrabble-game — project guide
Multiplatform Scrabble game. Read this first every session. The owner drives the project one stage per session (tariff constraint), so the repository — not conversation memory — is the source of continuity. Keep it that way.
Sources of truth (read before changing behaviour)
PLAN.md— staged plan + stage tracker + per-stage open details to interview.PRERELEASE.md— pre-release hardening tracker (phases R1–R7 before Stage 18); same per-phase interview + bake-back discipline asPLAN.md.docs/ARCHITECTURE.md— architecture, transport, security, the decision record. Always describes current state.docs/FUNCTIONAL.md(+_rumirror) — per-domain user stories. English authoritative.docs/TESTING.md— test layers + the per-stage CI gate.docs/UI_DESIGN.md— theuivisual/interaction design system.
Mandatory per-stage workflow
Start of a stage
- Read
PLAN.md(the stage's scope + open details) and the relevantdocs/. - Analyse what the stage actually requires against the current code.
- Interview the owner on every open detail and any fork not already fixed in the plan — do not silently pick borderline decisions. Offer options with brief pros/cons.
- Only then implement, strictly within the stage's scope.
End of a stage
- Bake every new agreement back into
PLAN.md,docs/ARCHITECTURE.md,docs/FUNCTIONAL.md(+_ru), the affected serviceREADME, and Go Doc comments — in the same PR. Correct earlier stages' docs/code if a new decision changes them. - Update the stage tracker; add a line under Refinements logged during implementation for any plan deviation.
- Get CI green, then mark the stage done.
(The stage-implementation skill encodes this same loop and can be invoked.)
Conventions
- All code, comments, identifiers, commits, docs, filenames in English.
- Chat with the owner follows the user-level
~/.claude/CLAUDE.md(Russian, the agreed persona and translation rules). - Mirror every point edit of
docs/FUNCTIONAL.mdintodocs/FUNCTIONAL_ru.mdin the same patch (translate only the touched paragraphs). - Prefer compact code; do not add deps, seams or knobs until a stage needs them. Reuse before adding. Document added packages/types/funcs with Go Doc comments.
- Update or add tests for every functional change.
Branching & CI
- Two long-lived branches (Stage 16 onward):
developmentis the integration branch;masteris the production trunk. Cutfeature/*branches fromdevelopmentand PR them back into it. (Stages 0–15 usedmasteras the trunk withfeature/* → master; the genesis Stage 0 commit is onmasterby necessity.) - A commit to a
feature/*branch triggers nothing. The single workflow.gitea/workflows/ci.yamlruns the full suite (unit+integration+ui) on a PR intodevelopmentormaster, and the gateddeployjob auto-rolls the test contour on a PR into — or a push to —development(docker compose up -d --buildon the runner host + aGET /probe). A PR intomasteris test-only. - Merge
development → masteronly when CI is green; the prod deploy is then a manual workflow (Stage 18), never automatic. Secrets/variables are prefixedTEST_/PROD_per contour (Gitea 1.26 has no deployment environments). - After any push, watch the run to green before declaring a stage done — use the
ready-made watcher, never an inline poll loop:
python3 ~/.claude/bin/gitea-ci-watch.py(background). It reads$GITEA_URL/$GITEA_TOKEN;gitea.iliadenisov.ruis allow-listed in.claude/settings.json. Remote:origin git@gitea.iliadenisov.ru:developer/scrabble-game.git.
Stack
Go 1.26.3, go.work monorepo, module paths scrabble/<name>. Dependencies are
added when first used (incremental): backend uses gin + zap +
pgx/go-jet/goose/OTel (added in Stage 1). Client↔gateway is Connect-RPC +
FlatBuffers (h2c); gateway↔backend is REST/JSON + X-User-ID plus a gRPC
server-stream for live events. UI is pure HTML5/CSS on plain Svelte + Vite,
packaged to native with Capacitor. Likely no Redis.
Reused engine: ../scrabble-solver (module scrabble-solver, Go 1.26.3)
Embedded in-process as a library — there is no per-game container. Public API to reuse (do not reimplement):
scrabble.NewSolver(rs, finder)→GenerateMoves(b, r, mode)(ranked, highest score first),ValidatePlay(b, dir, tiles),ScorePlay(...);scrabble.Apply(b, m); typesMove/Word/Placement/Direction/Mode(scrabble-solver/scrabble/{solver,move,apply}.go).rules.English() / RussianScrabble() / Erudit()(scrabble-solver/rules/rules.go).board.New / Parse / Clone / Transpose;rack.New / Add / Remove / Clone;selfplay.NewBag / Draw / Len(bag pattern).- Load committed dictionaries with
dawg.Load(path)fromgithub.com/iliadenisov/dafsa:scrabble-solver/dawg/{en_sowpods,ru_scrabble,ru_erudit}.dawg.
Constraints:
- Words/tiles are alphabet-index bytes, meaningful only with the matching
rules.Ruleset(Alphabet.Decode); blank flag carried separately. Decode to real characters before persisting history (history must be dictionary-independent — seedocs/ARCHITECTURE.md§9.1). - The solver's
internal/*is NOT importable from this sibling module. - GCG is test-only in the solver (no public writer) — we ship our own.
- Wiring: add
replace scrabble-solver => ../scrabble-solvertogo.workin Stage 2 (wheninternal/enginefirst imports it), and make CI check out the solver sibling (https://gitea.iliadenisov.ru/.../scrabble-solver.git). It uses publishedgithub.com/iliadenisov/{alphabet,dafsa}(no local replace).
Repository layout
go.work # use the existing modules; grows per stage
backend/ # module scrabble/backend
cmd/backend/ # main: telemetry -> db+migrate -> cache -> server
cmd/jetgen/ # dev tool: regenerate go-jet code (throwaway container)
internal/config/ # env config (composes postgres + telemetry)
internal/telemetry/ # OTel providers + request-timing middleware
internal/postgres/ # pgx/database-sql pool, goose migrations/, jet/ (generated)
internal/account/ # durable accounts + identities (store)
internal/session/ # opaque tokens, sessions store, cache, service
internal/server/ # gin engine, /api/v1 groups, X-User-ID, probes
internal/inttest/ # //go:build integration Postgres-backed tests
docs/ .gitea/workflows/ PLAN.md CLAUDE.md README.md
gateway/ ui/ pkg/ # added by their stages
platform/telegram/ # Telegram connector side-service (Stage 9): bot + gRPC API
backend/Dockerfile gateway/Dockerfile platform/telegram/Dockerfile # multi-stage distroless (Stage 16)
deploy/ # docker-compose + caddy + otelcol/prometheus/tempo/grafana (Stage 16)
Build & test
go build ./backend/... # per module ('./...' from the root won't span the workspace)
go vet ./backend/...
gofmt -l . # must print nothing
go test -count=1 ./backend/...
go build ./platform/telegram/... && go test ./platform/telegram/... # Telegram connector (Stage 9)
go run ./backend/cmd/backend # /healthz, /readyz on :8080
cd ui && pnpm install && pnpm check && pnpm test:unit && pnpm build # the UI (Stage 7+)
pnpm start # UI mock mode: lobby -> game, no backend
docker build -f backend/Dockerfile -t scrabble-backend . # images (Stage 16); gateway embeds the UI
docker build -f gateway/Dockerfile -t scrabble-gateway .
docker compose -f deploy/docker-compose.yml config # validate the full contour
The ui module is a Node project (pnpm), not in go.work; it is the ui job
of the single .gitea/workflows/ci.yaml (Stage 16 folded the former go-unit /
integration / ui-test workflows into it). Committed edge codegen under ui/src/gen/
(regenerate with pnpm codegen); pnpm build-script approval lives in
ui/pnpm-workspace.yaml (allowBuilds: esbuild: true).