docs(modules): settle Phase 5 — the Integration Kit, and the sidecar rule

Phase 4 closed; this is the plan for Phase 5, the Integration Kit, settled with
the org lead on six decisions.

MODULE_SYSTEM.md
- §2.11.1 (new): Phase 5's shape — measured starting state, the six decisions,
  the four slices, the acceptance mechanism.
- §2.11: layer 2 rewritten. The sidecar is the default and only path to a game,
  and it is a non-blocking dumb forwarder that persists before it forwards. A
  game that already ships a remote-control surface gets a thin sidecar, not none.
- §2.10: a MODULE_API_VERSION bump now carries a pass over the kit.
- Part 3: decisions 28–33.
- The Phase 5 line in Part 2's phase list: STARTED 2026-08-12.

MODULE_API.md
- §2.7 gains one prohibition: a module does not open a connection to a game
  server from the website process — a game socket, RCON, a query port, an
  engine admin API — with the three reasons it exists (the website is the
  internet-facing process, the sidecar owns the durable copy, neither side can
  stall the other). Normative prose with no CI behind it, stated so that every
  second module does not re-decide it.
- §1.1: the 1.4.0 entry. No member changed; minor deliberately.

modules/rust-dryrun.md
- §2 "Talking to the game" rewritten from "No sidecar" to a thin RCON sidecar,
  with a dated Correction note recording what the exercise originally concluded
  and why it was overruled. Corrected in view rather than silently rewritten.

Pairs with website#… (the 1.4.0 bump, into edge); either order.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-12 09:38:07 -05:00
parent 91ac28e11d
commit 89aff5c26b
3 changed files with 169 additions and 22 deletions

View File

@@ -26,13 +26,23 @@ here extends the contract first, in this file, before the module is written agai
Core exports a single integer-major semver string from `server/src/modules/version.js`:
```js
const MODULE_API_VERSION = '1.3.0'
const MODULE_API_VERSION = '1.4.0'
```
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
module chunk evaluates, which is earlier than any network round trip could answer.
**1.4.0 — Phase 5, the sidecar rule.** §2.7 gained one prohibition: a module does not open a
connection to a game server from the website process. It talks to a **sidecar**, which owns the
durable copy of the game's state. No member was added, removed or changed — the surface is identical
to 1.3.0 — and the bump exists because a module written against 1.3.0 could conform to every member
and still be built the wrong way round. It is **minor rather than major** deliberately: nothing that
satisfied 1.3.0's *surface* stops working, `module-uo`'s `coreApi: "^1.3.0"` still resolves, and
module-uo already complies because the sidecar is where it came from. The reasoning a module author
needs is [`../../Integration-kit`](https://gitea.whitlocktech.com/RunicGateway/Integration-kit)
chapter 3; the rule itself is below, because the kit teaches and never re-specifies.
**1.3.0 — Phase 3 slice 3, the client half's move.** Three client additions, each because the
extraction needed it: a nav item may carry an **`icon`** component (§3.3), core declares a third slot
**`player.invite.accepted`** (§3.7), and `window.__rg.api` gained **`BASE`** — which §3.5 specified
@@ -441,6 +451,33 @@ them leaves an operator with orphaned data and no supported way to remove it.
- Read `process.env` for core configuration. Its own config is a `settings` key or its own table.
- Call `process.exit`, install signal handlers, or start a listener.
- Write outside `ctx.paths.moduleRoot` and the upload directory.
- **Open a connection to a game server from the website process** — a game socket, an RCON channel,
a query port, an engine's admin API. A module talks to a **sidecar**, and the sidecar talks to the
game. Added in 1.4.0.
**Why the sidecar is not optional** (added 2026-08-12; the reasoning, and the worked example, are
Integration Kit chapter 3 — [`MODULE_SYSTEM.md`](MODULE_SYSTEM.md) §2.11):
- **The website is the internet-facing process and the game is not.** A module dialling the game
directly makes the public web app the thing the game trusts, and puts the game's address in the
same process as every request from the internet. With a sidecar, the game dials **out** and opens
no listening port at all, which is the invariant [`../link/PLAN.md`](../link/PLAN.md) exists to
hold.
- **The sidecar owns the durable copy.** It is a non-blocking dumb forwarder that **persists before
it forwards** — `uo-link` writes every event and every board snapshot to SQLite (`store.rs`) and
serves its REST reads from there. So a website that is down, restarting or mid-deploy loses
nothing, and a page renders the last thing the game said instead of going blank. A module holding
the connection itself has nowhere to put what arrives while the website is not running.
- **Neither side can stall the other.** The game's plugin enqueues onto a bounded drop-oldest queue
and a writer thread drains it; the live feed is best-effort and lossy on purpose (a lagging
consumer drops frames) because durability is the store's job, not the socket's. A module that owns
the socket inherits both problems inside an Express process, where the failure mode is a wedged
request handler.
This one is **normative prose with no CI behind it** — an outbound socket is not statically
detectable the way an internal `require` is (§5.1), so it is enforced in review. Stated as a rule
anyway, because the alternative is that every second module re-decides it, and the first one to
decide wrong finds out during an outage.
### 2.8 The OpenAPI fragment