# Working agreements Read this before changing anything. It is written for coding agents, and it is just as valid for humans. [`docs/architecture.md`](docs/architecture.md) explains *why* the pieces are shaped this way; this file is *how to work in them*. ## Orientation | I want to change… | Go to | | --- | --- | | schools, the game clock, game rules | `src/HSchool.Simulation` | | the menu API (list, create, delete) | `src/HSchool.Server/Api` **and** `docs/protocol.md` | | what the socket carries | `src/HSchool.Protocol` **and** `src/HSchool.Client/src/net/protocol.ts` **and** `docs/protocol.md` | | connection handling, the loop | `src/HSchool.Server` | | what runs locally | `src/HSchool.AppHost/AppHost.cs` | | screens, dialogs, formatting, UI language | `src/HSchool.Client/src` | ## Commands ```bash dotnet build h-school.sln ``` ```bash dotnet test ``` ```bash npm --prefix src/HSchool.Client test ``` ```bash npm --prefix src/HSchool.Client run build ``` ```bash dotnet run --project src/HSchool.AppHost ``` `run-aspire.cmd` is the same command for Windows users who want a double-clickable entry point — keep the two in sync if the AppHost path ever moves. `dotnet run --project src/HSchool.AppHost` starts the server *and* the Vite dev server and opens the Aspire dashboard. The Vite port is assigned per run (`npm run dev -- --port `), so read the client URL off the dashboard instead of assuming 5173. Do not start a dev server with a bare `npm run dev` when you meant to run the whole app — the client only finds the backend through the Aspire-injected `SERVER_HTTP` environment variable, or the `localhost:5180` fallback that matches the server's own launch profile. ## Invariants These are the rules that keep the base coherent. Breaking one is a design decision, not a detail — say so explicitly in the change description. 1. **The server is authoritative.** The client sends intents and draws what it is told. No game logic in `src/HSchool.Client` — not even a local clock that ticks between frames. 2. **The protocol lives in three places at once.** `ProtocolCodec.cs`, `protocol.ts` and `docs/protocol.md` change in the same commit. A layout change bumps `ProtocolConstants.Version` / `PROTOCOL_VERSION`. Tests on both sides assert byte offsets — if one of them has to change, so do the other two. 3. **Only the loop thread touches a `School` or the `SchoolRegistry`.** Everything inbound goes through `GameCommandQueue`; menu reads use the immutable state the loop publishes; everything outbound goes through the per-client outbox. No locks around the registry, no `Task.Run` into it. 4. **The simulation knows nothing about the network.** `HSchool.Simulation` must not reference ASP.NET Core, sockets or logging infrastructure. It stays testable without a host. 5. **Fixed timestep.** The clock advances by `SimulationOptions.FixedDeltaTime`, never by wall-clock deltas and never from `DateTime.Now`. Same tick count, same date. 6. **Everything from the wire is untrusted.** Validate lengths and ranges before anything reaches the simulation — names, dates and speed indexes all arrive from a browser. 7. **One intent per message.** A frame that also resends a neighbouring field overwrites it with a stale client copy; that is why running and speed are separate messages. ## Conventions **C#** - File-scoped namespaces, `var` where the type is obvious, primary constructors for services. - Private fields are `_camelCase`; `.editorconfig` enforces it. - Nullable is on everywhere. Don't add `!` to silence it; fix the flow. - Internal by default in `HSchool.Server`; public only where another project consumes it. - New tunables go on `SimulationOptions` with a default, not as a constant buried in a class. **TypeScript** - `strict` is on, no `any`, no non-null `!` assertions. - Relative imports carry the `.ts` extension (bundler resolution is configured for it). - Modules stay thin: `net/` speaks to the server, `ui/` renders, `format/` formats, `i18n/` holds the RU/EN dictionaries, `main.ts` wires them together. - No framework, plain DOM. `ui/dom.ts` is the whole helper budget. - UI strings go through `t(...)` in `i18n/strings.ts`, never inline. Game dates go through `format/gameTime.ts`, which follows the active locale. **Both** - Comments explain *why*, not *what*. Assume the reader can read code. - Match the surrounding style rather than introducing a new one. ## Testing policy - Simulation changes need a `GameClock` or `SchoolRegistry` test. They are fast and need no host. - Protocol changes need a round-trip test **and** a byte-layout assertion on both sides. - Server wiring, endpoints and the WebSocket belong in `tests/HSchool.AppHost.Tests`. That suite shares one AppHost across all tests (`AppHostFixture`) — keep it that way, booting per test costs about ten seconds each. - Those tests also share one server, so schools survive between them: start each test by clearing the list (`SchoolApiTests.ResetAsync`) instead of assuming it is empty. - The screens have no unit tests — a DOM environment would cost a dependency the project does not have. Verify UI changes by running the app. Dictionaries and date formatting are covered in Vitest (`i18n/strings.test.ts`, `format/gameTime.test.ts`). ## Dependencies - NuGet versions are centrally managed in `Directory.Packages.props`. Add the version there and a bare `` in the project. - Transitive pinning is on, so a downgrade warning means you bump the central version rather than adding a per-project override. - Keep the dependency count low. Arch, Aspire and the test runners are the whole budget. ## Things that will bite you - **``'s `close` event is not delivered by every engine** (Chromium 148 fires only `toggle`). `ui/modal.ts` resolves its promise explicitly on every exit for that reason — never go back to awaiting the event, or a dialog will silently freeze the screen that awaited it. - **Game dates are UTC on the wire and UTC when formatted.** A `DateTime` bound from configuration arrives as `Kind=Unspecified`, serializes without a `Z`, and the browser then reads it in its own time zone — `SimulationOptions.DefaultStartDate` forces the kind for exactly that reason. - `PeriodicTimer` does not catch up on its own. The accumulator in `GameLoopService` does, capped at 5 steps — do not "simplify" it away. - Every school runs whether or not a connection is watching it, so anything you hang off the tick runs six times over once six schools exist. `OpenSchool` only subscribes to clock frames. - The menu polls `GET /api/schools` once a second and patches its cards in place. Rebuilding the list on every refresh would drop focus and swallow clicks. - The client outbox drops the oldest frame under pressure. That is correct for clock frames and wrong for anything that must arrive exactly once — such a message would need its own path. - `erasableSyntaxOnly` is off in `tsconfig.app.json` on purpose: constructor parameter properties are used throughout.