Phase 5 slice 0 (MODULE_SYSTEM.md §2.11.1). The repo's governance, the front page, the book's outline, and the CI that keeps the whole thing from rotting. README.md What the reader is building, all three parts, and the draft banner: the kit is finished when someone outside this project builds a working module by following it alone, and that has not happened. Says the sidecar rule plainly (MODULE_API.md §2.7) rather than leaving it to chapter 3, because a reader who skims the front page and starts coding should still get that one right. book/README.md The outline of four chapters, landed before the prose so the shape can be argued with. Chapters are named but NOT linked — a link to a file that does not exist is what the link check is for, and an outline should not be the first thing to fail it. CONTRIBUTING.md The rule that governs every change here: the kit never re-specifies a contract. Also the prose conventions, and why the pinned ref points at core's `edge` rather than `main`. SECURITY.md Scoped for a repo that runs nothing: the two things that ARE reportable are a template that teaches an insecure pattern (it is meant to be copied) and a chapter that teaches something dangerous. scripts/checkLinks.js Relative links resolve; anchors match a real heading; no link pins a reader to a commit snapshot of a moving document. Nothing is fetched — a self-hosted Gitea would fail on a credential-less runner and teach us to ignore red. Fences and code spans are stripped by a line walk, not a regexp. Its first run found a real one: a PR template's relative links resolve from the REPO ROOT, because that is where their text ends up when Gitea inlines them into a pull request body. Encoded, with the reason. scripts/checkCoreApi.js The anti-rot check. Asserts template/module.json's `coreApi` EQUALS the pinned core's MODULE_API_VERSION — equality, not "satisfies", because a range check stays green across a contract bump and green would then mean "the template still loads" instead of "someone has re-read the book". Both failure branches and the pass were exercised against a real core checkout. ci/core-ref.json The pin, same convention as Module-uo's. Points at `edge`: core's `main` has no server/src/modules/ until the cutover, and that pin is one of the things the cutover has to revisit. .gitea/workflows/pr-checks.yml Two jobs. `links` always runs; `template` is conditional on template/module.json existing, so the repo is gated now and the job arms itself when slice 1 lands, with no edit to the workflow. Same guard Module-uo used through its planning phase. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Read the dry run before any of them.
| # | 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 |
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.
1. Your first module in twenty minutes
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.
- What the pieces of
template/are, one paragraph each. module.json: the fields you must change, andcoreApi.- Building the client chunk. Why a module ships prebuilt and an operator never builds anything.
- Installing it: the admin panel, the
MODULESenvironment variable, or a directory on the volume. - Reading the state your module lands in, and the four ways it can fail to load.
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); whatctxhands you and why each member is handed rather than imported; the lazy-accessor pattern that lets a ported file keep a file-scoperequire, 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.sqlinstead. - 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 andexternal: [], 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-linkas 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-pluginsas the worked example. The constraints are general; the C# is not.