diff --git a/docs/about-project.md b/docs/about-project.md index 4d3b1ca..192bc67 100644 --- a/docs/about-project.md +++ b/docs/about-project.md @@ -215,12 +215,22 @@ pnpm stdb:start pnpm stdb:publish:clear ``` -Generate frontend bindings after schema/reducer changes: +Generate frontend bindings after schema/reducer changes. + +The root script regenerates only `apps/tma/src/module_bindings`: ```bash pnpm stdb:generate ``` +The payments bindings are not covered by that script and must be regenerated separately: + +```bash +spacetime generate --lang typescript --out-dir apps/payments/src/module_bindings --module-path apps/spacetime/spacetimedb +``` + +Both commands must be run after every SpacetimeDB schema change. + Run frontend locally against cloud: ```bash @@ -263,3 +273,90 @@ pnpm test:stdb-local-scenarios:full - The frontend provider contract still includes `submitMove()` as the UI-facing action, but the SpacetimeDB provider implements it by committing and revealing under the hood. - `packages/shared/src/constants.ts` defines `MAX_ROUNDS = 9`, while `apps/spacetime/spacetimedb/src/index.ts` defines `MAX_ROUNDS = 5`. This may be intentional or may cause behavior differences between mock/shared logic and real backend. - The backend duplicates parts of game logic instead of importing `packages/shared`, so matrix and energy changes must be kept synchronized. + +## Architecture Analysis + +### Dependency Graph + +``` +@elmental/shared + └── consumed by: apps/tma, apps/server + └── not consumed by: apps/payments (independent domain, no shared dep) + +apps/tma + └── depends on: @elmental/shared, spacetimedb SDK, socket.io-client, zustand, telegram-apps/sdk + +apps/server (legacy) + └── depends on: @elmental/shared, express, socket.io, ioredis, pg + +apps/payments + └── depends on: spacetimedb SDK, undici + └── no dependency on apps/tma, apps/server, or @elmental/shared + +apps/spacetime/spacetimedb + └── single-file module, cannot import workspace packages + └── generates bindings consumed by: apps/tma, apps/payments +``` + +No app imports from another app. No circular dependencies exist. + +### Layer Separation + +The monorepo has four distinct runtime layers: + +| Layer | App | Responsibility | +|-------|-----|----------------| +| Frontend | `apps/tma` | React UI, Telegram SDK, provider contract | +| Authoritative backend | `apps/spacetime/spacetimedb` | Game state, reducers, matchmaking, settlement — this is the server | +| Payments | `apps/payments` | Stars invoicing, refunds, wallet history, admin | + +`apps/server` is a deprecated Express/Socket.io experiment that predates the SpacetimeDB architecture. It is not part of any active flow. TMA has no references to it; it is absent from `docker-compose.selfhost.yml`. Do not route gameplay through it. All server-side game logic — matchmaking, move resolution, balance mutations, timeouts — lives in SpacetimeDB reducers. If a new server-side component is needed, extend `apps/spacetime/spacetimedb` or create a dedicated new app. + +### What Is Properly Shared + +`packages/shared` is the correct place for the shared frontend/test game rules layer. It contains: + +- `types.ts`: canonical enums and interfaces (`MoveId`, `GameMode`, `RoundResult`, `MatchState`) +- `constants.ts`: energy values, move costs, regen tables, ELO parameters, economy constants +- `game-logic.ts`: `resolveRound`, `calculateElo`, `calculateEnergy`, `resolveOverclock` + +Frontend UI uses `@elmental/shared` for rendering decisions (move costs, labels). Production gameplay is server-authoritative: SpacetimeDB is the source of truth for real matches. The SpacetimeDB module duplicates equivalent game rules because it cannot import workspace packages — this is an architectural constraint, not a design choice. Backend rules and `packages/shared` must be kept in sync manually. + +### Known Duplications + +**1. Game constants and logic — `packages/shared` vs `apps/spacetime/spacetimedb/src/index.ts`** + +SpacetimeDB modules compile to a single self-contained file and cannot consume npm workspace packages. As a result, move costs, energy constants, regen values, and the outcome matrix are defined in both places. Any change to game rules requires a matching update in both files. `scripts/check-matrix-parity.mjs` exists to catch divergence in the move matrix, but does not cover all constants. The `MAX_ROUNDS` discrepancy (9 in shared, 5 in spacetime) is an example of what can silently drift. + +**2. Telegram `initData` validation — `apps/server/src/auth/index.ts` and `apps/payments/src/telegramInitData.ts`** + +Both implement HMAC-SHA256 validation of Telegram `initData` for the same purpose, but with different implementations. `apps/payments/src/telegramInitData.ts` is the current reference: it uses a timing-safe hex comparison (`timingSafeHexEqual`) and requires a valid `auth_date` (missing or expired → reject). The legacy `apps/server` version uses a direct string comparison (`===`) and treats `auth_date` as optional — only checked if present. Because `apps/payments` does not depend on `apps/server` (and should not), this duplication is currently necessary. If `@elmental/shared` ever gains a browser/Node-compatible crypto utility layer with no external dependencies, this validation could move there, using the payments implementation as the basis. + +**3. SpacetimeDB `module_bindings` — `apps/tma/src/module_bindings` and `apps/payments/src/module_bindings`** + +These are auto-generated from the same SpacetimeDB schema and are expected to be identical at any given schema version. They are not manually authored. Do not consolidate them into a shared package — they must stay co-located with each consumer so that `pnpm stdb:generate` can target each app independently. + +### Layer Violation Checks + +No violations were found at the time of this analysis: + +- `apps/tma` does not import from `apps/server` or `apps/payments`. +- `apps/payments` does not import from `apps/tma` or `apps/server`. +- `apps/server` does not import from `apps/tma` or `apps/payments`. +- All three apps consume `@elmental/shared` only through the workspace package name, not via relative paths. +- Generated `module_bindings` are not imported outside their owning app. `spacetimeProvider.ts` is the only non-generated file in `apps/tma` that may import from `src/module_bindings`. + +### Synchronization Risks + +The following pairs must stay in sync manually and are not enforced by the type system: + +| What | Location A | Location B | Guard | +|------|-----------|-----------|-------| +| Move outcome matrix | `packages/shared/src/game-logic.ts` | `apps/spacetime/spacetimedb/src/index.ts` | `pnpm test:matrix-parity` | +| Energy and move cost constants | `packages/shared/src/constants.ts` | `apps/spacetime/spacetimedb/src/index.ts` | None — manual | +| `MAX_ROUNDS` | `packages/shared/src/constants.ts` (= 9) | `apps/spacetime/spacetimedb/src/index.ts` (= 5) | None — diverged | +| SpacetimeDB schema | `apps/spacetime/spacetimedb/src/index.ts` | `apps/tma/src/module_bindings`, `apps/payments/src/module_bindings` | Run both generate commands from **Common Commands** — `pnpm stdb:generate` only covers `apps/tma`; payments must be regenerated separately | + +When changing game rules, update both `packages/shared` and `apps/spacetime/spacetimedb/src/index.ts`, then run the full verification sequence described in **Common Commands**. + +**Schema/bindings rule:** every SpacetimeDB schema change must regenerate bindings for every active consumer (`apps/tma` and `apps/payments`). If a consumer intentionally receives no updated bindings — for example, because a new table or reducer is not relevant to it — that decision must be recorded explicitly in the PR description.