Files
Integration-kit/template/README.md
wtclaude f8f7014d53
All checks were successful
PR Checks / prose (pull_request) Successful in -38s
PR Checks / template (pull_request) Successful in 29s
fix(kit): everything the acceptance run found — Phase 5 slice 3
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>
2026-08-12 14:40:02 -05:00

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:

  1. 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.
  2. The MODULES environment variable, <id>@<version>=<manifest URL>, for a deployment that declares its module set rather than clicking it.
  3. 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 regeneratednpm 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 regeneratednpm install --prefix client
swagger-fragment.json regeneratednpm 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.