# 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`](PLAN.md) — staged plan + **stage tracker** + per-stage *open
  details to interview*.
- [`PRERELEASE.md`](PRERELEASE.md) — pre-release hardening tracker (phases R1–R7
  before Stage 18); same per-phase *interview + bake-back* discipline as `PLAN.md`.
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — architecture, transport,
  security, the decision record. Always describes current state.
- [`docs/FUNCTIONAL.md`](docs/FUNCTIONAL.md) (+ [`_ru`](docs/FUNCTIONAL_ru.md)
  mirror) — per-domain user stories. English authoritative.
- [`docs/TESTING.md`](docs/TESTING.md) — test layers + the per-stage CI gate.
- [`docs/UI_DESIGN.md`](docs/UI_DESIGN.md) — the `ui` visual/interaction design system.

## Mandatory per-stage workflow

**Start of a stage**
1. Read `PLAN.md` (the stage's scope + *open details*) and the relevant `docs/`.
2. Analyse what the stage actually requires against the current code.
3. **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.
4. Only then implement, strictly within the stage's scope.

**End of a stage**
1. Bake every new agreement back into `PLAN.md`, `docs/ARCHITECTURE.md`,
   `docs/FUNCTIONAL.md` (+ `_ru`), the affected service `README`, and Go Doc
   comments — in the **same** PR. Correct earlier stages' docs/code if a new
   decision changes them.
2. Update the stage tracker; add a line under *Refinements logged during
   implementation* for any plan deviation.
3. 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.md` into `docs/FUNCTIONAL_ru.md`
  in 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): **`development`** is the
  integration branch; **`master`** is the production trunk. Cut `feature/*`
  branches **from `development`** and PR them back into it. (Stages 0–15 used
  `master` as the trunk with `feature/* → master`; the genesis Stage 0 commit is
  on `master` by necessity.)
- 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 + a `GET /` probe). A PR into
  `master` is test-only.
- Merge `development → master` only when CI is green; the **prod** deploy is then a
  **manual** workflow (Stage 18), never automatic. Secrets/variables are prefixed
  `TEST_` / `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.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>`. 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)`; 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`); blank flag 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.
- Wiring: add `replace scrabble-solver => ../scrabble-solver` to `go.work` in
  **Stage 2** (when `internal/engine` first imports it), and make CI check out
  the solver sibling (`https://gitea.iliadenisov.ru/.../scrabble-solver.git`).
  It uses published `github.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 side-service, two binaries (Stage 9; split in phase TX): cmd/validator (HMAC, no VPN) + cmd/bot (Bot API; dials gateway over reverse mTLS bot-link)
loadtest/                        # module scrabble/loadtest: the pre-release stress harness (R2)
backend/Dockerfile gateway/Dockerfile platform/telegram/Dockerfile loadtest/Dockerfile  # multi-stage distroless (Stage 16; loadtest R2); gateway/Dockerfile has the `landing` target (R3), platform/telegram/Dockerfile has `validator`+`bot` targets (TX)
deploy/                          # docker-compose (per-service limits, R7) + caddy + landing + otelcol (OTLP + docker_stats per-container metrics) + prometheus/tempo/grafana + postgres_exporter
```

## Build & test

```sh
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 (Stage 9; split in TX)
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 SPA
docker build -f gateway/Dockerfile --target gateway -t scrabble-gateway .
docker build -f gateway/Dockerfile --target landing -t scrabble-landing .  # static landing (R3)
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`).
