docs: scaffold the Integration Kit — front page, outline, and the checks
All checks were successful
PR Checks / template (pull_request) Successful in 6s
PR Checks / links (pull_request) Successful in 7s

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>
This commit is contained in:
2026-08-12 09:47:22 -05:00
parent f9694be6a0
commit cffd525bdf
13 changed files with 991 additions and 0 deletions

99
book/README.md Normal file
View File

@@ -0,0 +1,99 @@
# 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][dryrun] 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, 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.
## 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.
[dryrun]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust-dryrun.md