docs(book): the four chapters — Phase 5 slice 2
All checks were successful
PR Checks / prose (pull_request) Successful in 8s
PR Checks / template (pull_request) Successful in 27s

The book, written out of the tree slice 1 proved. Four chapters in the order the
work happens: the first module in twenty minutes, the website module, the sidecar,
and the game-side plugin.

Shape, settled with the org lead:

  * template/README.md stays the REFERENCE — it travels with a copied template and
    CI holds it against the tree — and chapter 1 is the narration: what you should
    see after each step, the state your module lands in, and the four ways it fails.
    The chapter links to the checklist rather than restating it.
  * chapters 3 and 4 cite link/ and servuo-plugins/ by FILE AND IDENTIFIER, never by
    line. Those repositories move for their own reasons and checkLinks already
    forbids commit permalinks, so a line number in this book is wrong the moment
    they do. The template stays the only code quoted verbatim.
  * one PR: the outline's status table and the link check are only coherent when the
    whole set lands.

scripts/checkChapterPaths.js is the anti-rot half a machine can answer: every path
a chapter names in backticks must exist. None of those mentions is a markdown link,
so checkLinks never looked at them, and none is code, so nothing else did either —
renaming one template file would have left four chapters quietly pointing at
nothing. Its anchor list is STATED rather than derived from the tree, for the reason
the template's own build guard states it: a list derived from what exists cannot
fail when what exists changes, and an anchor that stops matching is a check that has
silently stopped checking. So each anchor must exist or the check fails. Eleven
tests, every "must not catch" case a span that really appears in the book.

stripFences moved to scripts/lib/markdown.js and both checks use it — shared code,
not a shared description.

CHAPTER 1 WAS RUN, NOT REASONED ABOUT. The template was copied into a real core on
edge, booted against the dev database, and every claim in "what you should see"
checked: the five log lines, /examplegame/status with its injected
<script type="module" src="/modules/examplegame/entry.js">, the chunk served
no-cache while module.json 404s, /api/v1/public/world/status, the capabilities in
/api/v1/public/modules, and the route in the merged /api/docs.json. Then the three
failures the chapter tells a reader to cause on purpose, because a chapter that
predicts the wrong debugging heuristic is worse than one that predicts none:

  * an undeclared prefix  -> stage `register`, "declared public/extra but never
    registered it", routes 404 and absent from /public/modules;
  * a table without the id prefix -> stage `schema`, at LOAD time, before mounting;
  * a throwing onBoot -> after mounting, so the same route answers 503 "Module
    unavailable" rather than vanishing.

All three came out exactly as written, and the messages in the chapter are that
core's own. Two small corrections fell out of the run: the log sample now shows the
real interleaving of core's three lines with the module's two, and the section on
failure adds that a module disappears from /api/v1/public/modules in every failure
case — a check that needs no login.

MODULE_SYSTEM.md 2.11.1 slice 2. Docs half: docs#146.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-12 13:20:29 -05:00
parent 24b9d30a16
commit f41ff92c67
11 changed files with 1438 additions and 121 deletions

View File

@@ -1,99 +1,40 @@
# The book
Four chapters, in the order the work happens. **None of them are written yet**
this is the outline, landed first so the shape can be argued with before the prose
exists. Chapter status is in the table; a chapter that is not there yet is not
there yet, rather than a stub that reads like an answer.
Four chapters, in the order the work happens.
Read [the dry run][dryrun] before any of them.
Read [the dry run][dryrun] before any of them — a complete module designed on
paper for a second game, and the shortest honest picture of the whole job.
| # | Chapter | File | Status |
| --- | --- | --- | --- |
| 1 | Your first module in twenty minutes | `01-first-module.md` | not written |
| 2 | The website module | `02-website-module.md` | not written |
| 3 | The sidecar | `03-sidecar.md` | not written |
| 4 | The game-side plugin | `04-game-plugin.md` | not written |
| # | Chapter | What it covers |
| --- | --- | --- |
| 1 | [Your first module in twenty minutes](01-first-module.md) | Copy the template, rename it, build it, install it, see a page. No theory. |
| 2 | [The website module](02-website-module.md) | The bulk of the work: `module.json`, `register(ctx, api)`, the schema fragment, the client chunk, packaging, and what a module must never do. |
| 3 | [The sidecar](03-sidecar.md) | Why the website never talks to a game server, what "persist before you forward" means, and what a *thin* sidecar is. |
| 4 | [The game-side plugin](04-game-plugin.md) | The least code and the highest stakes: never block the game thread. |
They are named but not linked on purpose: a link to a file that does not exist is
the thing this repo's link check is for, and an outline should not be the first
thing to fail it.
Chapters 1 and 2 quote `template/`, which CI builds against a pinned core, so their
code is a tree that is proved rather than prose that looks like one. Chapters 3 and
4 cite `uo-link` and `servuo-plugins` by file and identifier rather than by line, on
purpose: those repositories move for their own reasons and a line number in a book
is wrong the moment they do.
## 1. Your first module in twenty minutes
## What is normative, and what is here
Copy `template/`, rename it, build it, install it, see a page. No theory. The point
is to reach a working module before learning anything, so that everything after it
is a change to something that already runs rather than a step toward something that
might.
Nothing in these chapters is. Where a chapter and one of these disagree, the
document is right and the chapter has a bug — [say so][issues]:
- What the pieces of `template/` are, one paragraph each.
- `module.json`: the fields you must change, and `coreApi`.
- Building the client chunk. Why a module ships **prebuilt** and an operator never
builds anything.
- Installing it: the admin panel, the `MODULES` environment variable, or a directory
on the volume.
- Reading the state your module lands in, and the four ways it can fail to load.
| Authority | For |
| --- | --- |
| [`MODULE_API.md`][api] | Everything a module may do. |
| [`MODULE_SYSTEM.md`][system] | Why the module system is shaped this way, and how a module is installed and removed. |
| [`link/PLAN.md`][linkplan] + [`INTEGRATION.md`][linkint] | The game↔sidecar wire protocol, as one real sidecar implements it. |
## 2. The website module
The bulk of the kit.
- **`module.json`** — every field, and which are load-bearing at boot.
- **The server entry point.** `register(ctx, api)`; what `ctx` hands you and why
each member is handed rather than imported; the lazy-accessor pattern that lets a
ported file keep a file-scope `require`, and the require-order rule that comes
with it.
- **The `register*` calls** — routes per tier, notification streams, announce legs,
post hooks, extension slots. Worked examples of each, with the distinctions that
are easy to get wrong (a leg is one-shot delivery with retry; a post hook is
idempotent state that also runs on delete).
- **The schema fragment.** Idempotent, replayed every boot, leading-verb allowlist,
the table-prefix rule, and why there is no migration runner anywhere in this
project. What belongs in `purge.sql` instead.
- **The client half.** The prebuilt ESM chunk; `window.__rg`; the shared-dependency
rule (core owns React and hands it over — a module that resolves its own gets two
Reacts and a broken page); the Vite library build with anchored aliases and
`external: []`, and *why* that combination rather than the obvious one.
- **Routes, nav and features on the client**, and how a module's nav row becomes an
ordinary row an operator can reorder, relabel or hide.
- **The UI kit** — seven members, closed on purpose. What to do about the eighth
thing you want.
- **The OpenAPI fragment**, and how to generate it from your own registrations.
- **Packaging and release CI**: the tarball, the install manifest, the checksum,
and the version living in `module.json`.
- **Boundaries.** What a module must not do, each with the failure it prevents.
## 3. The sidecar
Why it exists, why it is **not optional**, and what "thin" means for a game that
already speaks a remote-control protocol.
- The invariant: your game is never network-reachable; it **dials out**, the
sidecar listens, and only the website's backend talks to the sidecar.
- **Persist before you forward.** The sidecar owns the durable copy — event
history, the latest snapshot of every board, whatever a page must still be able
to render when the game or the website is down. A live feed is allowed to be
lossy *because* the store is not.
- The wire as a **versioned compatibility contract** rather than a build
dependency: a version on every response, a mismatch refused rather than
mis-parsed, and what a bump obliges you to change in the same commit.
- Auth, and why the sidecar is the only exposed part.
- `uo-link` as the worked example, and what a *thin* sidecar for an RCON-style game
keeps and drops.
## 4. The game-side plugin
The chapter with the least code and the highest stakes: a plugin that gets this
wrong takes the game down when the sidecar wedges.
- **Never block the game thread.** Enqueue and return; a bounded, drop-oldest queue;
a dedicated writer thread that drains it. Dropping the oldest event is correct,
and stalling the game to avoid it is not.
- **Read the world only on the game's own thread**, and hand plain data to the
writer.
- Reconnect, backoff, and what to send on connect so the sidecar can rebuild its
picture without asking.
- What to emit at all: the difference between an event stream and a state snapshot,
and why both exist.
- `servuo-plugins` as the worked example. The constraints are general; the C# is not.
The chapters teach: the order to do things in, the reasoning, and the mistakes that
cost this project time.
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
[system]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_SYSTEM.md
[dryrun]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust-dryrun.md
[linkplan]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md
[linkint]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/INTEGRATION.md
[issues]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit/issues