A cold agent was given this repo and the documents it links to, and nothing else — no core source, no module-uo — and asked to build a module for a second game. It did, in one pass. The record is docs/modules/kit-acceptance.md; this is the repair list, plus the two things it recommended that were not defects. The one it could not find, because it had no core to render against: a module page built exactly as this kit teaches renders OUTSIDE the site. PublicLayout is the chrome, not the body. Core grew an opt-in `shell` prop for it (MODULE_API_VERSION 1.5.0, website#148); the template passes shell="narrow" and chapter 2 explains why you name a width and never a class. Fixed: - **F1, and the worst of them, because it lands in the first twenty minutes.** `npm run check:swagger` failed on a PRISTINE template on Windows: the check compared the committed fragment byte-for-byte and a default Windows clone is CRLF while the generator writes LF. The message blamed "the routes or their annotations". Now `template/.gitattributes` pins `eol=lf` and the comparison normalises line endings anyway — a check may only fail for the reason it names, and this one names a diagnosis. - **F3** — `.gitea/workflows/release.yml` carries `gitea.example.com` and `your-org/your-module` under a literal `# CHANGE THESE`, was not in the rename checklist, and `checkRenameSites.js` could not match it, so CI was silent by construction. Row added, pattern widened. (The agent reported both workflow flavours; only the Gitea one is affected — GitHub supplies its own variables. Corrected in the record.) The near-miss is kept in the check's comments and its suite: the obvious widening is `example\.com`, which fires on a fixture URL in checkImports.test.js. Every alternative has to be a string that cannot occur by accident, which is the same rule that made the id `examplegame`. - **F4** — the release bundle's include list was hardcoded, so adding `server/utils/` would have silently dropped it from every release while the bundle check stayed green. Inverted to an exclusion list, in both flavours, and run by hand because a release workflow never executes in CI. - **F5** — the annotation-quoting warning was wrong in both directions, and the correction is measured rather than reasoned. A backtick is harmless (the template's own description has two spans and they survive). A `"` is not, and it does not throw: `'A "quoted" status'` is silently TRUNCATED to `A "` while swagger-autogen prints Success and the error capture sees nothing. The only signal is check:swagger blaming your routes. - **F6** — `template/.gitignore`, so a copied template that is `git init`ed inherits ignore rules instead of nothing. - **F7** — the UI kit is eight exports across five rows, not seven. The contract said seven and this kit had faithfully carried the miscount out of it. Adopted, not defects: - Chapter 1 now says to run every check on the untouched copy first. That is what found F1; without a baseline the first failure is ambiguous forever. - The template ships the §2.7 self-check the agent wrote for itself. The rule has no CI in general — an outbound socket is not statically detectable — but a module can make a decidable claim about its own tree. Ported from its code with a header explaining how to NARROW it when a sidecar client arrives, since talking to your sidecar is the expected shape and is not what §2.7 forbids. The pin moves to website edge 4ad8b2b, the 1.5.0 bump, and template/module.json declares ^1.5.0 — so checkCoreApi's equality assertion still holds and the template uses a member that exists only at that ref and later. 32 server + 18 client template tests, 21 kit-script tests, all four checks green. Co-Authored-By: Claude <noreply@anthropic.com>
7.1 KiB
The template module
A Runic Gateway module that builds, loads, and does almost nothing. Copy it, rename it, and you have a running module before you have read a chapter.
Installed into a core, it adds:
- one public page at
/examplegame/status, and a nav row pointing at it; - one API route,
GET /api/v1/public/world/status, described in an OpenAPI fragment core merges into its own/api/docs; - one table,
examplegame_world_status, created by an idempotent schema fragment and dropped by a purge file; - both lifecycle hooks, so there is something to see at boot and at shutdown.
That is deliberately less than your module will do. What it is complete about is the shape: every seam a real module uses is here once, with the reasoning next to it, and CI proves the whole thing still builds against a pinned core.
The tree
module.json what core reads first — id, version, coreApi, mounts
server/
index.js register(ctx, api) — the entire server-side handshake
core.js the lazy accessors over ctx; read this second
boot.js onBoot / onShutdown
db/schema.sql idempotent, replayed every boot
db/purge.sql destructive, run only by an explicit admin purge
model/worldStatus/ the .db.js / .model.js pair
router/public/ one router, one controller, the #swagger annotations
swagger/doc.js tags and schemas the annotations refer to
scripts/checkImports.js the module boundary, enforced
scripts/swaggerFragment.js generates swagger-fragment.json from your own routes
test/ the suites — start with entry.test.js
client/
vite.config.js the library build: anchored aliases, external: []
src/entry.jsx registers routes and nav at evaluation time
src/core.js what core hands you: the UI kit (eight exports)
src/shim/ the four shared dependencies, re-exported from core
src/routes/public/ the page
scripts/checkExternals.js asks the BUILT chunk whether a bare import survived
test/ build.test.js and registration.test.js
.gitea/workflows/release.yml packaging CI — Gitea
.github/workflows/release.yml the same, for GitHub. Keep one, delete the other.
swagger-fragment.json generated; commit it
Neither workflow runs while it sits inside the kit — a workflow is only read from a repository root. They arm themselves when your copy is a repository of its own.
Build it
npm ci --prefix server
npm test --prefix server
npm run check:imports --prefix server
npm ci --prefix client
npm run build --prefix client # → client/dist/entry.js, the chunk that ships
npm run check:externals --prefix client
npm test --prefix client # build FIRST: two of these tests read the chunk
npm test in client/ passes with no build, by skipping the tests that need one.
That is on purpose — the suite has to be runnable before the build — and it means
a CI job that tests without building is a job asking nothing. Build first.
Regenerate the OpenAPI fragment whenever a route or an annotation changes:
npm run swagger --prefix server # writes swagger-fragment.json
npm run check:swagger --prefix server # fails if it is stale
Install it
Three supported ways, and none of them builds anything on the operator's machine:
- Admin → Modules, pasting the URL of an install manifest — the JSON the release workflow attaches beside the tarball. This is how an operator installs your module.
- The
MODULESenvironment variable,<id>@<version>=<manifest URL>, for a deployment that declares its module set rather than clicking it. - A directory on the volume. Copy this whole tree to
<website>/modules/<id>/and restart. The fastest loop while you are developing.
For (3): copy, do not symlink. The loader lists directory entries and a symlink is not a directory, so a linked module is skipped in silence.
Rename it
Change id in module.json first, then work down the list. Nothing here is
subtle, and the suites catch most of a half-finished job: schema.test.js fails
the moment a table name stops matching the id, and registration.test.js fails
when a nav row stops matching its route.
Your id must match ^[a-z][a-z0-9-]{1,31}$, must equal the directory name core
loads you from, and becomes your table prefix — so no hyphen unless you enjoy
backticking table names.
| File | What to change |
|---|---|
module.json |
id, name, version, the mounts prefix, capabilities |
.gitea/workflows/release.yml |
GITEA_HOST and REPO, under the # CHANGE THESE banner — the only two, and they are wrong until you do. (The .github/ flavour needs nothing: GitHub supplies GITHUB_REPOSITORY and friends.) |
server/package.json |
package name and description |
server/core.js |
the message every accessor throws |
server/boot.js |
the placeholder world name |
server/db/schema.sql |
every table name — the prefix must be your id |
server/db/purge.sql |
the same table names |
server/model/worldStatus/worldStatus.db.js |
the TABLE constant |
server/router/public/world.router.js |
the #swagger.tags name |
server/swagger/doc.js |
the tag, and the Examplegame… schema prefix |
server/scripts/swaggerFragment.js |
the generated fragment's info.title |
server/test/_fakes.js |
ctx.moduleId |
server/test/worldStatus.test.js |
the fixture's world name |
server/package-lock.json |
regenerated — npm install --prefix server |
client/package.json |
package name and description |
client/vite.config.js |
the guard plugin's name |
client/src/core.js |
the console tag on the identity check |
client/src/shim/rg.js |
the console tag on the missing-global error |
client/src/entry.jsx |
ID, and every route path and nav to |
client/test/registration.test.js |
the example path in the comment |
client/package-lock.json |
regenerated — npm install --prefix client |
swagger-fragment.json |
regenerated — npm run swagger --prefix server |
That table is checked. scripts/checkRenameSites.js at the root of this kit
compares it against the tree on every pull request: a file that still mentions the
placeholder and is not listed fails the build, and so does a listed file with
nothing left to rename. A checklist nobody verifies is a checklist that is wrong
by the second edit.
Two things you do not rename: the mount prefix /world need not be your id
(the server's prefix namespace is shared with core's, and /status, /settings,
/version and /contact are already taken), and the world / worldStatus
naming throughout is ordinary vocabulary you should replace with your own domain's
when you replace the feature.
Licence
GPL-3.0-or-later, like everything else in this project — see LICENSE.md. This directory is meant to be copied and made yours; it carries that licence, and so does anything derived from it.