# 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` |
| defs, JSONC catalog, map validation | `src/HSchool.Content` |
| skills, traits, body, needs, countries | `src/HSchool.Content` |
| people generation, families, roster records, yearly intake | `src/HSchool.People` |
| timetable planning | `src/HSchool.Schedule` |
| routes, day plans, who comes today | `src/HSchool.Ai` |
| people in a school's World, need decay, walking | `src/HSchool.Simulation` |
| the menu API (list, create, delete, mods, catalog) and the people list/card | `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, workers, saves | `src/HSchool.Server` |
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
| screens, dialogs, formatting, UI language | `src/HSchool.Client/src` |
| clothing, inventory, climate, dress codes | [`docs/design/inventory.md`](docs/design/inventory.md) — slice 7, phases 29–37; country, wardrobes and weather are live |
| login, who owns a school, watching others | [`docs/design/session.md`](docs/design/session.md) — slice 8, phases 38–39 |
| clock speed buttons and high-speed stride | `src/HSchool.Simulation/ClockSpeed.cs` **and** [`docs/design/session.md`](docs/design/session.md) — phase 40 |
| talks, opinions, fights, romance pack | [`docs/design/social.md`](docs/design/social.md) — slice 9, phases 41–47 |
| lesson consequences (teacher present, skill, warmth, textbook, weather commute) | [`docs/design/off-queue.md`](docs/design/off-queue.md) — phases 48, 50–53 |
| layer tests, client tsc-in-test, worker/card splits, dump nodes | [`docs/design/craft.md`](docs/design/craft.md) — slice 10, phases 56–59 |
| a bug in already-shipped behaviour | `.claude/skills/bug-work` — document in `docs/bugs/`, discuss only if the fix has a fork, then patch |
| a task that is not in any slice | `.claude/skills/side-work` — discuss, document in «Вне очереди», then implement |
| a new named slice of the game | `.claude/skills/slice-work` — discuss, write design and phases, then `/phase-orch` |
## Commands
```bash
dotnet build h-school.sln
```
```bash
dotnet test tests/HSchool.People.Tests --filter FullyQualifiedName~YearlyIntake
```
```bash
npm --prefix src/HSchool.Client test -- src/ui/peoplePanel.test.ts
```
Solution-wide `dotnet test` and a full client `npm test` are CI, or when the user asked. Agents
scope the run — Testing policy.
```bash
npm --prefix src/HSchool.Client run build
```
```bash
dotnet run --project src/HSchool.AppHost
```
`run-aspire.ps1` is the Windows entry point (`run-aspire.cmd` just forwards to it);
`run-aspire.sh` is the same on Ubuntu. They skip MSBuild when AppHost and Server dlls are
newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on
PATH and connected they run `tailscale serve --bg 5173` and print the HTTPS client URL;
otherwise they warn and continue locally only. Pass `--no-tailscale` to skip Serve. Keep
`tools/apphost-uptodate.ps1`, `LaunchBuildStamp`, and `needs_build` in `run-aspire.sh` in
sync if the AppHost path ever moves.
`dotnet run --project src/HSchool.AppHost` still compiles every time — that is for a dirty tree,
not a fast relaunch. It starts the server *and* the Vite dev server and opens the Aspire
dashboard. The Vite client listens on port **5173** (pinned in AppHost for Tailscale Serve).
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 a school's worker thread touches that `School` or its `World`.** The supervisor
owns the mailbox table and never calls into a world. Create/delete/name suggestions go
through `GameCommandQueue`; open/close/running/speed go to that school's mailbox; menu
reads use the snapshots workers publish; everything outbound goes through the per-client
outbox. No locks around a school, 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
**What to run.** One project from the list below, filtered to the new or changed class while
iterating. Client Vitest only if `src/HSchool.Client` changed. `HSchool.AppHost.Tests` only if
the change is HTTP, WebSocket, or host wiring — it boots a server. Do not `dotnet test` the
solution, do not run client and .NET together "to be sure", do not re-run after a merge that
only resolved a status line. Solution-wide is CI.
- Simulation changes need a `GameClock` or `SchoolRegistry` test. They are fast and need no host.
Putting a roster into `World` and ticking needs belongs there too.
- People generation belongs in `tests/HSchool.People.Tests`. Same seed, map, country and
native language must produce the same roster; the suite does not boot a host.
- Timetable planning belongs in `tests/HSchool.Schedule.Tests`. Same staff, map and locks must
produce the same table; the suite does not boot a host.
- Walking and day plans belong in `tests/HSchool.Ai.Tests`. Same seed and map must produce the
same route and commute; the suite does not boot a host, and it does not reference Arch.
- Catalog, inheritance, patches and map validation belong in `tests/HSchool.Content.Tests`.
Feed the loader documents, not disk paths.
- 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 (and now on disk too):
start each test by clearing the list (`SchoolApiTests.ResetAsync`) instead of assuming it
is empty. Reset deletes through the API, which deletes the save files. Headless AppHost
sets `HSchool:AllowSaveReload` so tests can `POST /api/dev/reload-schools` without killing
the shared fixture.
- Launch-script up-to-date checks belong in `tests/HSchool.AppHost.Tests` and must not use
`AppHostFixture`. Keep `LaunchBuildStamp` and `tools/apphost-uptodate.ps1` in sync.
- Screen logic is covered in Vitest under happy-dom (`ui/*.test.ts`): people filters and the
pager, the create dialog (core stays on, map reset, submit busy), planner rejection text, and
the payroll-cap message. Layout and styles are still verified by running the app. Dictionaries
and date formatting stay in `i18n/strings.test.ts` and `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
- **`