Files
scrabble-game/CLAUDE.md
T
Ilia Denisov fca8fd038b
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
docs: settle the export docs on the final delivery matrix
ARCHITECTURE: the time zone rides the URL too; 'only degraded platform'
(legacy TG) instead of 'only remaining branch' — the delivery is
per-platform by design now. CLAUDE.md: the renderer joins the build/test
commands and the Dockerfile list.
2026-07-02 23:47:13 +02:00

9.5 KiB

scrabble-game — project guide

Multiplatform Scrabble game, in production at https://erudit-game.ru. Read this first every session. The repository — not conversation memory — is the source of continuity; keep it that way.

Sources of truth (read before changing behaviour)

How we work

  • Inspect the relevant code path and the docs above before changing behaviour.
  • Interview the owner on every fork — do not silently pick borderline decisions; offer options with brief pros/cons.
  • Smallest correct diff. Prefer compact code; reuse before adding; do not add deps, seams or knobs until they are needed.
  • Update or add tests for every functional change, at the layers docs/TESTING.md calls out.
  • Bake docs in the same PR: update docs/ARCHITECTURE.md, docs/FUNCTIONAL.md (+_ru), the affected service README and Go Doc comments alongside the change.
  • Document added packages, types, funcs, consts and vars with Go Doc comments.

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.md into docs/FUNCTIONAL_ru.md in the same patch (translate only the touched paragraphs).

Branching, CI & production

  • Two long-lived branches: development is the integration branch; master is the production trunk. Cut feature/* from development and PR back into it; promote development → master via PR when ready to release. Both branches require one approval + the CI / gate check.
  • A commit to a feature/* branch triggers nothing. The single workflow .gitea/workflows/ci.yaml runs the full suite (unit + integration + ui) on a PR into development or master, and the gated deploy job auto-rolls the test contour on a PR into — or a push to — development (docker compose up -d --build on the runner host + landing/SPA/backend probes). A PR into master is test-only.
  • Production is live on two hosts (main + the Telegram bot host) and deploys only manually (workflow_dispatch), never automatically:
    • .gitea/workflows/prod-deploy.yaml (confirm=deploy, from master) builds + pushes the images to the registry, then SSH-deploys both hosts — rolling per service in dependency order, health-gated, auto-rollback to the previous tag; a schema migration adds a maintenance window + a consistent pg_dump. Four visible jobs: build → deploy-main → deploy-bot → verify.
    • .gitea/workflows/prod-rollback.yaml (confirm=rollback) re-deploys a prior release (blank target_version = the previous deployed version) — image-only, rolling, health-gated.
    • Releases are git tags vX.Y.Z on master; the deploy stamps git describe --tags into the image tag, every binary (pkg/version via -ldflags → the service.version telemetry attribute) and the SPA About screen. Tag the release before deploying.
    • Hosts are provisioned idempotently by deploy/ansible/. Per-contour secrets/variables use the TEST_ / PROD_ prefix (Gitea 1.26 has no deployment environments). Migrations must be expand-contract (backward-compatible) so image rollback stays DB-safe. Full runbook + variable list in deploy/README.md.
  • After any push, merge or deploy, watch the run to green before declaring done — use the ready-made watcher (run it in the background), never an inline poll loop: python3 ~/.claude/bin/gitea-ci-watch.py. It reads $GITEA_URL / $GITEA_TOKEN; gitea.iliadenisov.ru is 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>. Backend uses gin + zap + pgx/go-jet/goose/OTel. 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. No Redis.

Reused engine: ../scrabble-solver (module scrabble-solver, Go 1.26.3)

Embedded in-process as a library (replace scrabble-solver => ../scrabble-solver in go.work; CI checks out the sibling from https://gitea.iliadenisov.ru/.../scrabble-solver.git). 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); types Move/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) from github.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); the blank flag is carried separately. Decode to real characters before persisting history (history must be dictionary-independent — see docs/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.
  • The solver uses published github.com/iliadenisov/{alphabet,dafsa} (no local replace).

Repository layout

go.work                      # the go.work monorepo
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
gateway/                     # module scrabble/gateway: Connect-RPC edge, embeds the SPA
ui/                          # Svelte + Vite SPA + landing (Node project, not in go.work)
pkg/                         # shared: telemetry, version, wire/FlatBuffers, proto, mtls
platform/telegram/           # Telegram side-service: cmd/validator (HMAC, no VPN) + cmd/bot (Bot API; dials gateway over reverse mTLS bot-link)
renderer/                    # image-render sidecar (Node + skia-canvas): runs ui/src/lib/gameimage.ts server-side for the finished-game PNG export
loadtest/                    # module scrabble/loadtest: the load/stress harness
docs/  .gitea/workflows/  CLAUDE.md  README.md
backend/Dockerfile gateway/Dockerfile platform/telegram/Dockerfile loadtest/Dockerfile renderer/Dockerfile  # multi-stage distroless (renderer: node:22-slim + skia-canvas + fonts); gateway/Dockerfile has the `landing` target, platform/telegram/Dockerfile has `validator`+`bot` targets
deploy/                      # docker-compose (+ prod overlay + bot host) + ansible provisioning + caddy + landing + otelcol (OTLP + docker_stats) + prometheus/tempo/grafana + node_exporter + postgres_exporter; prod-deploy.sh

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 validator + bot
go run ./backend/cmd/backend    # /healthz, /readyz on :8080

cd ui && pnpm install && pnpm check && pnpm test:unit && pnpm build  # the UI
pnpm start                      # UI mock mode: lobby -> game, no backend
cd renderer && pnpm install && pnpm test  # image-render sidecar (bundles ui/src/lib/gameimage.ts, skia smoke)

docker build --build-arg DICT_VERSION=v1.3.0 -f backend/Dockerfile -t scrabble-backend .  # DICT_VERSION required (no default); gateway embeds the SPA
docker build -f gateway/Dockerfile --target gateway -t scrabble-gateway .
docker build -f gateway/Dockerfile --target landing -t scrabble-landing .  # static landing
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. Committed edge codegen under ui/src/gen/ (regenerate with pnpm codegen); pnpm build-script approval lives in ui/pnpm-workspace.yaml (allowBuilds: esbuild: true).