From 0c5e40c50914ec7d49330f561ddd350a9745918a Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sat, 11 Jul 2026 22:53:26 +0200 Subject: [PATCH] docs: capture agent field notes; drop RuStore-steering for the GP variant --- .claude/CLAUDE.md | 177 ++++++++++++++++++++++++++++++++++++++++++++++ ANDROID_PLAN.md | 13 ++-- CLAUDE.md | 8 +++ 3 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 .claude/CLAUDE.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..9588de4 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,177 @@ +# Agent field notes — scrabble-game + +Non-obvious, hard-won project knowledge that is **not** in the main docs (`CLAUDE.md`, `docs/*`, +`deploy/README.md`). Kept in the repo so it travels with a clone to any host. These are working +memory, not a spec — **verify any named file / flag / function against the current code before acting +on it**, and prefer the authoritative docs where they overlap. Add to this file as new gotchas turn up. + +## Codegen & build + +- **jetgen churns everything.** `backend/cmd/jetgen` regenerates go-jet code for *all* tables and may + reorder output. After running it, revert the churn on tables you did not touch and commit only your + table's change. +- **flatc is version-pinned** (`pkg/Makefile`, `REQUIRED_FLATC = 23.5.26`, hard-checked). A different + flatc silently churns the generated wire code and can flip wire defaults. Never regenerate FBS with + another flatc version. +- **gopls lags codegen.** Right after an FBS/jet regen, gopls shows phantom "undefined" errors. Trust + `go build` / `go test`, not the editor squiggles. +- **go-jet type mapping:** SQL `numeric` → `float64`, `interval` → `string`. Never store money as + `numeric` (float precision loss) — use **bigint minor units + a `Money` type**; store durations as + **int seconds**, not `interval`. +- **`go mod tidy` chokes on dot-free local module paths** (`scrabble/...`). Hand-edit `go.mod` when a + bump is needed. The solver (`../scrabble-solver`) is consumed via `go.work` replace locally, but a + prod bump goes through a solver **PR → master + a published tag**, not a local replace. +- **Run the whole CI suite locally before pushing** — unit + integration (`//go:build integration`, + Postgres) + the UI job + codegen check. Do not lean on CI to catch what a local run would. +- **CI runner shares this host's `/tmp` as a different user.** In workflow steps, write artifacts to + `${GITHUB_WORKSPACE}`, never a fixed `/tmp/...` path (cross-user permission failures otherwise). +- **pnpm corepack pre-flight flakes.** `pnpm exec` / `pnpm check` occasionally abort on a corepack + pre-flight. Dodge it by invoking the tool directly: `node_modules/.bin/`. + +## Wire / schema evolution (client ↔ gateway ↔ backend) + +- **FBS is additive-only.** Add **trailing** fields ("added trailing — backward-compatible"). Never + delete or reorder a mid-table field — **deprecate** it (`(deprecated)`); deleting shifts field IDs + and breaks older readers. +- **Retiring a domain field must also retire the WIRE field it fed** — by deprecation, not deletion, + and do not just zero it: a dead `0` the client dutifully syncs will clobber the real value. +- **The gateway transcodes FBS ↔ backend JSON DTOs in lockstep.** A wire change usually means editing + both the FBS schema and the backend DTO. +- **Seat display name is built in two places** — `game.Service` live events **and** the server REST + DTOs. Change both or they drift. +- **A per-viewer flag is computed only in the per-viewer REST DTO**; the client seeds it from REST, + then bumps it from the live event. Don't expect it on the shared broadcast. + +## UI / Svelte 5 + +- **Never name a `$state` variable `state`.** `svelte-check` then misreads `$state` as a store + subscription. Rename (e.g. `view`). +- **Svelte trims literal edge whitespace** in markup. To keep a separator space, emit an expression: + `{' : '}`. +- **No global `.btn` / `.ghost` button classes.** Style buttons per-component with scoped CSS + design + tokens (mirror `NewGame`'s `.invite`). +- **A `$state` proxy fails structured-clone** when persisted to IndexedDB. Snapshot at the call site + with `$state.snapshot(...)` before storing. + +## Platform / WebView quirks (Telegram, VK, iOS, native, old Android) + +- **Telegram `showPopup` eats the user-activation.** Share / clipboard called from inside a + `showPopup` callback fail (no gesture). Use your own `Modal` for gesture-gated Web APIs. +- **iOS Telegram `` navigates away** and strands the SPA. Deliver files by **Web + Share on mobile**, and only use a `` Blob path on **desktop**. +- **Android TG/VK WebViews** lack `navigator.share` and ignore ``. Deliver a + client-generated file by **copying it to the clipboard**. +- **VK Android WebView ignores `target=_blank`.** Open external links through `lib/links.ts` + (routes to `vk.com/away.php`). Verify on-device. +- **Per-platform file-delivery last hop differs** (TG / VK / plain browser) — there is a delivery + matrix; do not re-litigate it without new on-device facts. +- **Old Android System WebView floor ≈ Chrome 67.** The bundle targets **es2019** (esbuild lowers + syntax), a conditional `core-js` polyfill loads only on old engines, and `index.html` has a boot + gate (BigInt / Proxy are the hard block → the unsupported-engine screen). There's also a `vmin` + glyph fallback for old rendering. +- **iOS WKWebView overscroll and Telegram swipe-to-close are not reproducible in Playwright.** Verify + those live on the deployed contour, not in the e2e. +- **Telegram Desktop Mini App shows a persistent bottom-right loader** — that's the long-lived + Subscribe stream, cosmetic, deliberately left as-is. + +## Testing + +- **UI test layers:** vitest (node env, pure logic, **no jsdom**) + Playwright **mock** e2e. The mock + e2e **bypasses the codec**, so wire/codec bugs need **codec unit tests**, not e2e coverage. +- **Mock overlay blocks e2e.** The cold-load overlay must be instant under the mock build or it + intercepts Playwright taps. +- **Mock tile pools lack a blank `'?'`.** The seeded game `G1` hard-codes one; flip `G1`'s variant to + eyeball per-variant tiles. +- **Durable Playwright MCP servers** (chromium + webkit) exist for UI inspection (there's a + plugin-config gotcha in wiring them). +- **`docker run -p ...` boot tests fail from the shell** (published ports unreachable in this env). + Use **testcontainers** for container-backed tests. +- **Distroless images run as UID 65532 (nonroot).** Bind-mounted TLS keys must be **0644** (not 0600) + or the service crash-loops on start. + +## Deploy / test contour (operational) + +- **The TEST contour runs on THIS dev host.** Inspect it via `docker` / Prometheus. A host-side + `curl` to caddy **hangs** (NAT hairpin) — don't debug the edge that way. +- **The contour's client IP is the home-router SNAT address**, not the real external IP. Correct in + prod, not a bug — which is why the IP ban / blocklist are **prod-only**. +- **The contour is one shared env, last-deploy-wins.** Keep a multi-PR batch a **linear stack** (one + PR, one deploy) so deploys don't clobber each other. +- **A schema/wire PR breaks the contour** until a `DROP SCHEMA` + backend restart. Note such a + prerelease step in `PRERELEASE.md`. +- **A contour DB wipe resets the account but not the client's stored locale**; the reconciler then + syncs the stale locale, masking a fresh TG `language_code` seed. Clear client prefs too when testing + locale. +- **`DNS=` in `TEST_AWG_CONF` pins the VPN netns to 1.1.1.1** → internal names go NXDOMAIN → the + bot-link silently dies. Diagnose from inside the netns. +- **Swap one contour service to a local image** without deploy secrets via a busybox-in-the-netns + socket-inspect trick (single-service recreate). +- **A rolling deploy did NOT recreate caddy on a config-only change.** Force `--force-recreate` for + caddy; don't trust "deploy green" for an edge-config change. +- **A new gateway edge route MUST be added to the Caddyfile `@gateway` matcher** or it falls through + to the landing catch-all. Add a CI probe for the route. +- **Prod caddy logs warnings only, no access log.** Trace a request via the backend "http request" + telemetry log or Tempo, not caddy. +- **Confirm a release is live without SSH** by grepping the served SPA: `__APP_VERSION__` inside + `/assets/main-*.js`. +- **"App hangs on load" was a dead HTTP/3 advert:** caddy sent `Alt-Svc: h3` with no UDP/443 open; + clients cache it ~30 days. Fix with `Alt-Svc: clear`. +- **Config-poison deploy loop:** a crash-looping config-mounted service + a missing bind source + produces a root-owned directory that then fails deploys. Break it by removing the root-owned dir. +- **Maintenance-window contract** (planned-deploy 503): a marker header, `/_gm` exempt, the flag spans + the whole roll, the SPA overlay reloads on recovery. Don't break these invariants. +- **A new dictionary goes live via a `/_gm/dictionary` upload, NOT a redeploy.** In-flight games keep + their pinned version. The **owner** does the upload. +- **Renderer deploy job flakes** on the skia-canvas GitHub binary download when its lockfile changes — + re-run, it's not your code. + +## Repo workflow + +- **PR-based, zero issues.** Work is tracked via PRs + `PRERELEASE.md`. "заведи задачу" means *do it* + / add a plan line — not file a tracker issue. +- **`tea` CLI for all Gitea ops** (PRs, secrets, variables, dispatch). `gh` does **not** work here. The + agent **cannot self-approve** a PR but **can merge** it after the owner approves. Watch the + stale-mergeable trap (re-check mergeability right before merging). +- **Watch every push/merge/deploy to green** with `python3 ~/.claude/bin/gitea-ci-watch.py`, launched + **bare** under a background task. It polls run-level conclusions; its `ALL GREEN` already covers the + gated deploy job. Pass `--no-runs 600` when the runner is busy. A **merge** is the most-forgotten + case — watch the post-merge runs too. +- **After a merge, switch to the merged-into branch** (`development`/`master`), pull, and prune the + local feature branch. +- **The contour deploy probe checks the backend `/readyz`**; a PR deploy builds the PR's own code; a + wedged contour can be recovered by recreating the host container set. + +## Domain semantics (not obvious from the code) + +- **Account deletion must NOT delete any user messages** — including feedback / support. Interview the + owner on every deletion point before wiring it. +- **`account.time_zone` is `NOT NULL DEFAULT 'UTC'`, seeded from a detected ±HH:MM offset.** An email + account row is created at the **code-request** step, not at confirmation. +- **The robot has two distinct time windows:** a **sleep window** (~00:00–07:00, gates its moves and + nudges) vs the **player away window** (turn-timeout only). "окно отсутствия" in dialogue = the + **sleep** window. + +## Production topology + +- **Two prod hosts.** `main` — full stack + ACME/edge (Selectel); `tg` — Telegram bot only (vdsina). + SSH aliases `scrabble-main-ops` / `scrabble-tg-ops`. Deploy is **manual dispatch only**, rolling + + health-gated + auto-rollback. Hosts are provisioned by `deploy/ansible/` (inventory + vars there — + the source of truth for IPs/roles). +- **The agent has targeted root SSH** to the hosts (and may run the prod Ansible). Surface every + owner-side step **before** a deploy, not after. + +## Feature-area pointers (state lives in the linked docs/plans, not here) + +- **Monetization** — «Фишка» currency, per-platform wallets, ads. Agreements in + `docs/PAYMENTS_DECISIONS_ru.md`; a phased plan (E0–E9). go-jet money rule above applies. +- **PITR** — pgBackRest → Selectel S3 (encrypted, 30-day), currently **gated off**; runbook in + `deploy/README.md`. Gotcha: run pgBackRest via docker-exec **as the `postgres` user** with + `--pg1-user=scrabble`. +- **Offline mode** — the robot brain, move generator/validator/scorer and DAWG reader are **JS ports** + of the Go engine, bundled client-side and **parity-pinned** by golden tests. Multi-phase; native + offline-first bundles the dictionaries (see `ANDROID_PLAN.md`). +- **VK ID web login** — raw OAuth 2.1 against `id.vk.com`, a **separate VK "Web" app** from the Mini + App, server-side confidential code exchange. +- **Email relay** — Selectel SMTP + confirm / link / unlink / deletion codes + alerts. +- **Native Android** — see `ANDROID_PLAN.md` (Capacitor bundle model, client-version gate, + offline-first, RuStore). diff --git a/ANDROID_PLAN.md b/ANDROID_PLAN.md index 3b49d49..959ea6f 100644 --- a/ANDROID_PLAN.md +++ b/ANDROID_PLAN.md @@ -459,12 +459,13 @@ One codebase, two build variants selected by flags: difference is that purchases are hidden — the flag structure (`purchasesHidden()`) already carries both cases, so no repaint is needed. -**Open compliance item to settle when the GP build is actually built (deferred, not now):** Google -forbids not just external billing but *steering* users to other payment methods or app stores. The -existing `isGooglePlayBuild()` stub "points the user to the RuStore build" — that steering likely -violates Google policy. When building for GP, change the stub to a neutral "not available here" with -no pointer to RuStore. Tracked here so it is not forgotten; it does not affect the RuStore MVP. Google -Play also requires an **AAB** (not APK) and its own signing/Play App Signing — a separate CI target. +**Google Play compliance — decided: no RuStore steering.** Google forbids not just external billing +but *steering* users to other payment methods or app stores. So the Google Play build hides purchases +with **no** pointer to RuStore: the existing `isGooglePlayBuild()` RuStore-pointer stub is **dropped** +in the GP variant (show nothing, or a neutral pointer-free message — never name or link RuStore or any +other store). This does not affect the RuStore MVP (there `isGooglePlayBuild()` is false, so the stub +never shows). Google Play also requires an **AAB** (not APK) and Play App Signing — a separate CI +target, built later. --- diff --git a/CLAUDE.md b/CLAUDE.md index dc03378..27db4a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,3 +156,11 @@ The `ui` module is a Node project (pnpm), **not** in `go.work`; it is the `ui` j 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`). + +## Agent field notes + +Non-obvious, hard-won knowledge the agent has accumulated that is **not** captured in the docs above, +kept in the repo so it travels with a clone. Verify any named file/flag against current code before +acting on it. + +@.claude/CLAUDE.md