Compare commits

...

10 Commits

Author SHA1 Message Date
c24443f721 Merge pull request 'docs(kit): re-pin to core 1.21.0 and teach report targets' (#19) from docs/core-1.21.0 into main
Reviewed-on: #19
2026-10-30 14:19:42 +00:00
3f5cd72ed6 docs(kit): re-pin to core 1.21.0 and teach report targets
All checks were successful
PR Checks / prose (pull_request) Successful in 11s
PR Checks / template (pull_request) Successful in 43s
ci/core-ref.json moves from 7bd93b8 (1.20.0) to f13b8fe (1.21.0,
website#233, report, block and hide). Chapter 2 gains a section on
registerReportTargets, registerGameIdentities and ctx.safety, and its call
count becomes twenty-three. template/module.json's coreApi becomes ^1.21.0
for the equality check; the template declares none of the new members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-30 09:04:26 -05:00
be767776ac Merge pull request 'docs(kit): pin core 1.20.0; teach onUserDeleting' (#18) from docs/core-1.20.0 into main
Reviewed-on: #18
2026-10-30 04:47:17 +00:00
36f29a9cac docs(kit): pin core 1.20.0; teach onUserDeleting
Some checks failed
PR Checks / template (pull_request) Successful in 54s
PR Checks / prose (pull_request) Failing after -24s
Moves ci/core-ref.json from 69998ec (1.19.0) to 7bd93b8 (1.20.0, website#232,
account deletion). Chapter 2 gains "When a member deletes their account": the
hook runs before the row goes, a cascade is not an unlink, it cannot stop the
deletion, so a module that talks to a game needs a record and a retry. Its call
count becomes twenty-one. The demo section now says core refuses multipart bodies
and data: URIs (website#229) on module routes without them being listed. The
template's coreApi becomes ^1.20.0 for the equality check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-29 22:59:09 -05:00
045679bbbc Merge pull request 'docs(kit): pin core 1.19.0; teach the public demo's module members' (#17) from docs/core-1.19.0 into main
Reviewed-on: #17
2026-10-26 11:02:54 +00:00
00f5234f97 docs(kit): pin core 1.19.0; teach the public demo's module members
All checks were successful
PR Checks / prose (pull_request) Successful in 10s
PR Checks / template (pull_request) Successful in 44s
The pin moves from 6890a74 (1.17.0) to 69998ec (1.19.0, website#224),
past 1.18.0 (website#223). Both are the public demo (DEMO_MODE.md §10
step 3). The chapters were read again for both versions:

- Chapter 2: a new section on a public demo site, covering demoLocks in
  module.json, ctx.demoMode and api.onDemoSeed (1.18.0). The call count
  becomes twenty.
- Chapter 5: a paragraph on an event action's demoRefuses (1.19.0), and
  a line in the action listing.

The one other core change in the window (website#221) is a wording fix
on core's Teams panel and nothing a module sees. The template declares
none of the new members, so template/module.json's coreApi becomes
^1.19.0 for the equality check only.

Checks run at the new pin:
- checkCoreApi, links, rename sites and chapter paths all pass, and so
  do the script tests (10 and 11).
- The template's imports, build, externals and swagger checks pass.
- Tests pass: server 82, client 20.
- The template was loaded as `examplegame` into a real core at this ref
  through core's own loader, with DEMO_MODE unset and with it true, and
  it registered both times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-26 05:58:34 -05:00
0297f81ae2 Merge pull request 'fix(ci): restore the missing comma in core-ref.json' (#16) from fix/core-ref-json into main
Reviewed-on: #16
2026-10-23 07:14:56 +00:00
2f018999c3 fix(ci): restore the missing comma in core-ref.json
All checks were successful
PR Checks / prose (pull_request) Successful in 10s
PR Checks / template (pull_request) Successful in 48s
The template job's "Clone core at the pinned ref" step reads the pin with
require('./ci/core-ref.json'), and the file has not parsed since
0678047 (2026-10-06): the "why" array lost the comma after "Before that
move:". So the job has failed on every PR since, run 15 and run 16 (#15)
included, before it reached a single check.

The local runs missed it because checkCoreApi takes the core as
`--core .core` and never reads this file. Only the CI clone step reads
it. The file now parses, and `ref` is 6890a74 as #15 set it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-23 02:09:48 -05:00
50e28dc3bd Merge pull request 'docs(kit): pin core 1.17.0; teach 1.13.0 to 1.17.0 and one module per site' (#15) from docs/core-1.17.0 into main
Reviewed-on: #15
2026-10-23 07:07:42 +00:00
5b3f5a5d54 docs(kit): pin core 1.17.0; teach 1.13.0 to 1.17.0 and one module per site
Some checks failed
PR Checks / template (pull_request) Failing after 12s
PR Checks / prose (pull_request) Successful in 14s
The pin moves from 0a37a44 (1.12.0) to 6890a74 (1.17.0, website#220),
past 1.13.0 to 1.16.0, which reached main in the same cutover. The
chapters were read again for all five versions:

- Chapter 5: a lease's example, an option row's note, a param's
  duration (1.13.0) and multiple (1.14.0), a retry's retryAfterMs
  (1.15.0), a dry run's warnings (1.16.0), and a new section on
  templates (1.13.0). "What to build, in order" gains a template as
  step 8.
- Chapter 2: a new section on the identity-link hooks and
  ctx.identities.list (1.17.0). The call count is corrected to
  nineteen; it said twelve, which was already stale at 1.10.0.
- Two core changes in that cutover carried no version bump, and both are
  taught. Chapter 1 now says a site runs one game module and the loader
  refuses the others, since a reader copying the template onto a UO or
  Rust core is the one it surprises. Chapter 5 now covers a resource's
  `until`, which reconcile reads.

The template declares none of the new members, so nothing it does
changed meaning. template/module.json's coreApi becomes ^1.17.0 for the
equality check.

Checks run at the new pin:
- checkCoreApi, links, rename sites and chapter paths all pass, and so
  do the script tests (10 and 11).
- The template's imports, build, externals and swagger checks pass.
- Tests pass: server 82, client 20.
- The template was loaded as `examplegame` into a real core of this code
  through core's own loader, and it registered. Loaded again beside
  module-uo, examplegame started and uo was refused with the reason
  chapter 1 quotes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-23 02:01:26 -05:00
5 changed files with 316 additions and 7 deletions

View File

@@ -135,7 +135,7 @@ whether it is a directory; a symlink answers no, and your module is skipped in
complete silence. This is the single most common way a first install appears to do
nothing at all.
Two more things that look like your module failing and are not:
Three more things that look like your module failing and are not:
- If core is running in a container, your files have to be on the volume core sees
— `MODULES_DIR` (`/app/modules` under the shipped Compose file), not the
@@ -143,6 +143,14 @@ Two more things that look like your module failing and are not:
- A core with a fresh database boots in **maintenance mode**, and public module
pages sit behind the same maintenance gate core's own do. Your page will look
broken while the site is not live yet.
- **A site runs one game module.** If the modules directory already holds one, an
Ultima Online or Rust site's for example, only the directory whose name sorts
first starts. Every other is refused before any of its code is read, and its
reason on Admin → Modules names both modules. `examplegame` sorts ahead of `rust`
and `uo`, so on such a core yours starts and the site's own module does not. Try
your module on a core of your own, with nothing else in its modules directory.
This is [`MODULE_SYSTEM.md` §2.5][system]; the installer refuses a second module
too, so the rule only surprises someone copying directories by hand.
## What you should see
@@ -270,6 +278,7 @@ what `ctx` hands you and why it is handed rather than imported, what each
module must never do.
[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
[issues]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit/issues
[renamecheck]: ../scripts/checkRenameSites.js
[acceptance]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/kit-acceptance.md

View File

@@ -147,9 +147,11 @@ statement of your dependencies, and it makes a test double for it — see
## What you register
Twelve calls — ten registrations and the two lifecycle hooks — all synchronous,
all listed in [§2.4][api]. What is worth knowing is not their signatures but the
model behind them.
Twenty-three calls, all synchronous and all listed in [§2.4][api]: seventeen
registrations, the two lifecycle hooks, the two identity-link hooks, the demo's
seed hook, and the account-deletion hook. Five of the
registrations are the event system's, and [chapter 5](05-events.md) has those. What
is worth knowing is not their signatures but the model behind them.
**Every call stages; nothing is committed until your whole module is known good.**
The shape of a claim is checked at the call, so a malformed one throws with your
@@ -505,6 +507,164 @@ warm-up it never finished, and handing it a half-built world to tear down is wor
than not closing cleanly. A module with no hooks at all still reaches `started`:
having nothing to warm up is not the same as never having started.
### When a player links a sign-in identity
If your game has its own idea of an account, a player's Steam id for instance, you
may want to know when somebody links that identity to their site account
(`MODULE_API_VERSION` 1.17.0). Linking stays core's. Core verifies the identity
with its provider and writes the link; your module may read what core verified,
refuse a link, and hear one, and nothing more.
```js
api.onIdentityLinking(async ({ userId, provider, subject }) => ({ ok: true }))
api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {})
// and, from anywhere: ctx.identities.list(userId, provider)
```
- **`onIdentityLinking` is asked before the link is written**, for every provider,
so filter on `provider` yourself. Answer `{ ok: false, reason: 'in_use' }` when
the identity already belongs to somebody else in your game; the person reads
core's own *"already linked to another user"*. **It fails closed**: a throw, or no
answer within 5 s, refuses the link. That is the right direction for a check whose
whole job is to stop one game account landing on two people.
- **Your `message` is logged, never shown.** Module text on the page would let
anybody craft a link that prints a sentence of their choosing on the account
screen.
- **`onIdentityLinked` is told after the row exists**, and is awaited for 5 s so
the page the person lands on already shows what you made of it. Its failure is
logged and the link stands, so write it to tolerate a race: your own record may be
taken between the check and this call, and "theirs stands" is the answer.
- **There is no unlink hook.** Unlinking removes only the sign-in, and a record your
module made from it is yours to keep or remove.
- Neither hook can mint a session or change who is signed in.
### When a member deletes their account
A member can delete their own site account, and staff can delete one from the
admin panel (`MODULE_API_VERSION` 1.20.0). Core removes the row, and every table
with a foreign key to it follows: `ON DELETE CASCADE` for what was theirs alone,
`SET NULL` for what they made that others still read. Your module hears about it
once, first:
```js
api.onUserDeleting(async ({ userId, deletedBy }) => {}) // deletedBy: 'self' | 'staff'
```
- **It runs before the row goes**, so you can still read what the person holds.
- **A cascade is not an unlink.** If your game holds anything for the person, such
as a link it reports back on every login or a grant it enforces, undo it here
through your module's own unlink, the same call a player's own unlink makes. That
way the game sees exactly what it would see if they had unlinked by hand. Clean up
any data you keep without a foreign key here too.
- **It cannot stop the deletion.** A throw is logged, and so is no answer within
10 s, and the account is deleted anyway, because a person who asks to be deleted
must be. So a module that talks to a game needs **a record of what is left and a
retry**: write down what you are about to undo *before* the first call, keep what
the game could not take (it was down, or it refused), and try again when the game
comes back. That record cannot have a foreign key to `users`, because it outlives
the user it names.
- **Your hook keeps running past the 10 s.** Core stops waiting, not your code, so
the user may already be gone when your last call finishes. Anything you write
afterwards must not need the row: an activity entry, for instance, names the id in
its `detail`, not its `userId`.
- **Every module that holds per-person game state should register it.** Declaring it
needs `coreApi` `^1.20.0`, because an older core has no `api.onUserDeleting`, and
calling a missing function throws during `register()`.
### When members report, hide or block what your game shows
If your module shows what players wrote, such as chat from the game, members can
report it, hide it for themselves, and block whoever wrote it, and staff can hide
it for everyone (`MODULE_API_VERSION` 1.21.0, [`USER_SAFETY.md`][safety]). Core
keeps the Reports queue and each member's blocks and hides. You keep the content,
and you tell core three things about it:
```js
api.registerReportTargets([{ id: 'mygame.chat', label: 'Chat line', snapshot, hide, unhide }])
api.registerGameIdentities([{ kind: 'steam', label: 'Steam account', ownersOf }])
// and, from your own per-reader routes: ctx.safety.forViewer(userId), ctx.safety.staffAmong(userIds)
```
- **`snapshot({ targetId, viewer })` answers for that reader, and `null` covers two
cases.** An item that is gone and an item they could not have seen get the same
`null`, so core gives them the same 404. Otherwise a report or a block would tell
anybody who said line 4410 on a server whose player list is hidden from them. Run
the same visibility check your page runs.
- **Name the author by their site account when they have one.** `author.userId` is
the linked account and `author.identity` is the game id. `ownersOf(ids)` answers
which ids are linked. A block then lands on the account, and only an unlinked id
is blocked as itself. Core rewrites that block as an account block once the id
links, so you never have to.
- **Filter on the server, in your query.** A route that shows the content to
signed-in readers calls `ctx.safety.forViewer(userId)` and leaves out what that
reader blocked or hid. Do it in the `WHERE` clause, so a page of 100 is still 100
long, and keep the route per-viewer (`Cache-Control: private`), or a shared cache
hands one reader's filter to the next. If your page offers *Block*, mark each item
with whether it can be blocked. Use `ctx.safety.staffAmong` for that, because
staff cannot be blocked.
- **Hide for everyone is a flag you keep.** `hide` is called from core's queue.
Leave the item out for everyone but staff, and show it to staff marked, with an
undo where it lives. Core never calls `unhide` itself.
- **Each call has 5 s.** A module that does not answer is a 503 to the member,
never a silent success. Declaring any of it needs `coreApi` `^1.21.0`.
Module-Rust's `server/safety.js` is a complete worked example: chat lines, Steam
ids and the feed's own filter.
### On a public demo site
A site can run as a public demo (`MODULE_API_VERSION` 1.18.0). Anybody signs in with
published logins, nothing destructive is allowed, and the site is wiped and seeded
again every couple of hours. It is the same image with one flag set,
`DEMO_MODE=true`. Core locks its own routes; yours are yours to declare, and three
members are for that. With the flag off none of them changes anything, except that
`demoLocks` is still validated.
```json
"demoLocks": [
{ "method": "PUT", "path": "/admin/mygame/servers/:id" },
{ "method": "DELETE", "path": "/admin/mygame/servers/:id" }
]
```
(The template has no write routes, so this is a shape rather than one of its files.)
- **`demoLocks` in `module.json` lists your write routes that the demo refuses.**
Core answers a match `403 { code: 'DEMO_LOCKED', message }` before your handler
runs, the same body it gives for its own locks, so the site's one toast explains
it. Writes only (`POST`, `PUT`, `PATCH`, `DELETE`), in your routers' own `:param`
syntax with an optional trailing `/*`, and under one of your declared `mounts`.
**It is validated on every boot, demo or not**, so a wrong entry fails on your
machine rather than only on the demo.
- **Two refusals reach your routes without being listed.** On a demo, core refuses
any request with a multipart body, and any write whose JSON body carries a
`data:` URI in any string (a pasted image is an upload by another name), with the
same `403` body. An upload route of yours is locked on a demo whether or not
`demoLocks` names it.
- **`ctx.demoMode` is for a lock a route list cannot express**, one that depends on
what is in the request rather than which URL it went to. The Rust module refuses a
console command by its first word, and a plugin's config file by the plugin that
owns it. Check it in your handler and answer with the same `403` body. It is also
how you hide the rare read that must never be shown on a demo, such as a file
holding credentials; `demoLocks` takes writes only.
- **`api.onDemoSeed(fn)` writes your rows into the demo's fresh database.** The seed
calls `fn(data, { accounts })`: `data` is your section of the fixture
(`modules.<id>`, with every `"$env:NAME"` string already replaced) and `accounts`
maps each demo username to its user id. Core never writes your tables. **Upsert,
never insert**: the seed may run more than once on one database, and core seeds
its own content only the first time but calls your hook on every run. A fixture section for a module with no hook fails the seed, and so does a
hook that throws.
- **Lock at the server, never in the UI.** A request sent with `curl`, or from an
old app build, must be refused exactly like a click. Your pages may explain a lock;
they must not hide or disable a control as the way of locking it.
- **Declaring `demoLocks` needs `coreApi` `^1.18.0`**, because an older core rejects
the unknown key. Using only the other two should declare it as well: an older core
has no `onDemoSeed`, and its `ctx.demoMode` is `undefined`, which reads as "not a
demo".
An event step the demo should refuse when it is saved is
[chapter 5](05-events.md)'s `demoRefuses`.
## The schema fragment
`template/server/db/schema.sql` is your tables. It is replayed **in full, at every
@@ -819,3 +979,4 @@ the next chapter is for.
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
[acceptance]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/kit-acceptance.md
[safety]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/USER_SAFETY.md

View File

@@ -89,6 +89,7 @@ api.registerEventLeases([{
id: 'examplegame.rate.gather',
label: 'Gather rate',
type: 'float', min: 0.5, max: 5,
example: 2, // optional, 1.13.0: the value box's placeholder
maxDurationMs: 48 * 60 * 60 * 1000,
async read() { /* the live baseline */ },
@@ -149,6 +150,7 @@ api.registerEventActions([{
async revert({ runId, resources, idempotencyKey }) {}, // required iff 'ledger'
async reconcile({ runId, resources }) {}, // optional
async progress({ runId, stepId, idempotencyKey, params, resources, viewer }) {}, // optional, 1.12.0
async demoRefuses(params) {}, // optional, 1.19.0
}])
```
@@ -167,6 +169,13 @@ second code path is a dry run of the second path. Validate everything you can
reach without writing, then stop. Answering `{ ok: true }` unconditionally makes
the dry run worthless in the one situation it exists for.
**A dry run may also advise** (MODULE_API 1.16.0). `{ ok: true, detail: { warnings:
["…"] } }` shows each line to the author as a warning on that step, and it never
fails the dry run. Use it for what is true of the world now and may not be when the
run starts: *"the spot is refused now: a player is within 50 m"*. Strings only, at
most ten per step, each cut at 500 characters. What would make the step fail
whenever it ran is not advice; answer that as a refusal.
**`progress` is read by strangers, so it answers for one reader** (MODULE_API 1.12.0).
While a run is live, core asks it how its step stands and puts the answer on the
event's public page and in the app.
@@ -181,6 +190,22 @@ event's public page and in the app.
already cache, not from a fresh round trip to the game per reader.
- **Failure.** A throw or a late answer costs a line, never the page.
**`demoRefuses` keeps a step off a public demo** (MODULE_API 1.19.0). A demo site
([chapter 2](02-website-module.md)) lets anybody author an event, and some steps must
never be saved there: the Rust module refuses a console command that would stop the
server. Core asks `demoRefuses(params)` only with `DEMO_MODE` on, for every step,
when a definition is created, saved or published, with the step's checked params.
- **The answer.** A sentence refuses the whole write with `403 DEMO_LOCKED`, and the
author reads it as written, so phrase it for them: *"quit" is turned off on the
demo site.* `null` lets the step through.
- **It is a pure check.** No database, no game, nothing changed. A throw refuses too,
with core's own sentence, because a lock that cannot decide fails closed.
- **Check again in `perform()`**, behind `ctx.demoMode`. A version published before
your list grew is never asked again, so the save cannot be the only lock.
- **It needs `coreApi` `^1.19.0`.** An older core never asks it, and the save goes
through without a word.
**`example` is required on every param, optional ones included.** It is the
authoring form's placeholder. It is one word at declaration time and it is
unreconstructable afterwards by anybody who did not write the action.
@@ -189,6 +214,48 @@ unreconstructable afterwards by anybody who did not write the action.
another module) registered. A source that refuses degrades its field to free text
with a warning and never blocks the form — so resolve from live data and return
`[]` on failure, rather than defending with a hardcoded list that will be wrong.
A row may carry a `note` (1.13.0), a sentence the editor shows under the field
while that row is chosen. It is advice about the value; nothing on the save path
reads it.
Two more things a param can say, both for the editor's sake:
- **`duration: 'minutes'`** (1.13.0), on an `int` or `float` that is a length of
time. The editor then knows the step holds something for that long, and sees to
it that the phase it sits in lasts at least as long. Declare it on every param
that is a duration; the editor has no other way to tell.
- **`multiple: true`** (1.14.0), with an optional `max` (1 to 100, 20 by default),
on a `string` or `int`. The author picks several values, and `perform()` and
`cost()` receive an array. A bare value arrives as a list of one, so a spec
written by hand still works. Price the whole list in `cost()`: three kits given
are three gives.
## Templates: a shortcut past the editor
A fifth declaration, `api.registerEventTemplates` (MODULE_API 1.13.0), gives an
author a named starting point for a new event: "A kit weekend", built from a kit
and two dates.
```js
api.registerEventTemplates([{
id: 'examplegame.beacon-night',
label: 'Beacon night',
description: 'Light the beacons at dusk, put them out at dawn.',
inputs: [ /* params, as an action's are, at most 8 */ ],
async build({ inputs }) { return { title, summary, spec: { phases } } },
}])
```
**A template is never a second way to make an event.** What `build()` returns is
checked as a spec and opened as an ordinary unsaved draft, and the author saves it
through the same route and the same checks as one they typed. So a template cannot
make an event the save path would refuse, and nothing about it is stored: changing
or removing one touches no event it ever made. When the inputs make no sense,
answer `{ ok: false, error }` with a sentence for the author; anything you throw
stays in the log.
Build one last, once your actions and leases are right. It is a convenience over
them, never a replacement for them.
---
@@ -233,6 +300,13 @@ silence and the operator sees `"<action id> refused"`. Writing this chapter's
template is how that was found — its first draft used `detail`, and every refusal
it produced was anonymous.
**A retry may say when** (MODULE_API 1.15.0). Core retries a failed step after a
flat minute. If your game knows better, as with a thing that is warming up for ten
minutes, answer `{ ok: false, retry: true, retryAfterMs }`. Core clamps it to one
to thirty minutes rather than refusing it, and it still counts as one attempt of
the step's few: it changes *when*, never *how many*. So a hint is not a way to wait
for something that may take an hour; refuse that, and say why in `error`.
## 2. Pass the idempotency key through, and put it on a command rather than a question
Core hands `perform()` an `idempotencyKey` derived from the step's identity — never
@@ -315,6 +389,13 @@ time is up), call `ctx.events.expired({ kind, ref })` when it does. Core then ma
row `expired`: finished as planned, never taken back at teardown. Left to `reconcile`,
the same thing would turn up `orphaned` and read as something that went wrong.
**And report its deadline when you make it.** A resource in `perform()`'s answer may
carry `until`, the instant the game will end it by itself (an ISO string or epoch
milliseconds). After an outage, core's reconcile can run before the game's own "it
ended" has reached it. A resource gone past its `until` is then filed `expired`, as
`ctx.events.expired()` would have filed it, instead of `orphaned`. Without `until`,
the same outage turns every timed thing your event made into an operator's alarm.
**You say WHEN to reconcile, because core cannot.** Core has no concept of the game
being up — it sees `{ ok: false, retry: true }` and cannot tell a wedged sidecar
from a game that rebooted and lost everything an event made. So it asks once, at
@@ -379,6 +460,8 @@ Four things a second module's author reaches for and should not.
Its absence is merely a lower standard rather than a broken promise.
7. **`progress`**, if a visitor would want to watch the step happen. It is the only
one an outsider reads, so build it once the ledger it counts from is right.
8. **A template**, if authors keep building the same event by hand. It saves them
typing and adds no power, so it comes last.
Then read your own `revert` again, and ask what it answers when the game is down.

View File

@@ -1,10 +1,66 @@
{
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
"branch": "main",
"ref": "0a37a44164ab107584b918e656651452b50bd4f3",
"ref": "f13b8fe1fec9dc330a985430b8bd4f67c9ec26c5",
"why": [
"The core this kit is written against, pinned to a commit rather than a branch.",
"",
"2026-10-30: moved from 7bd93b8 (1.20.0) to f13b8fe (1.21.0, website#233,",
"report, block and hide, USER_SAFETY.md). Chapter 2 was read again:",
"- 1.21.0: api.registerReportTargets, api.registerGameIdentities and",
" ctx.safety.forViewer / staffAmong (chapter 2 gains a section, and its call",
" count becomes twenty-three).",
"website#233 is the only core change in the window. The template declares",
"none of the new members, so nothing it does changed meaning; module.json's",
"coreApi becomes ^1.21.0 for the equality check. The real core run was",
"repeated at this ref, with the template as the only module in the directory.",
"",
"2026-10-30: moved from 69998ec (1.19.0) to 7bd93b8 (1.20.0, website#232,",
"account deletion, ACCOUNT_DELETION.md). Chapter 2 was read again:",
"- 1.20.0: api.onUserDeleting (chapter 2 gains a section, and its call count",
" becomes twenty-one).",
"The other core changes in the window (website#225-#231) are the public",
"demo's deploy, seed and locks. One reaches a module: website#229 refuses, on",
"a demo, any write whose JSON body carries a data: URI, beside the multipart",
"refusal 1.18.0 already had, and neither needs listing in demoLocks. Chapter",
"2's demo section now says so; it had not taught the multipart one either.",
"The template declares none of the new members, so nothing it does changed",
"meaning; module.json's coreApi becomes ^1.20.0 for the equality check. The",
"real core run was repeated at this ref, with the template as the only",
"module in the directory.",
"",
"2026-10-26: moved from 6890a74 (1.17.0) to 69998ec (1.19.0, website#224),",
"past 1.18.0 (website#223). Both are the public demo (DEMO_MODE.md), and the",
"chapters were read again for both versions:",
"- 1.18.0: demoLocks in module.json, ctx.demoMode and api.onDemoSeed",
" (chapter 2 gains a section, and its call count becomes twenty);",
"- 1.19.0: an event action's demoRefuses (chapter 5, a paragraph and a line",
" in the action listing).",
"The one other core change in the window (website#221) is a wording fix on",
"core's Teams panel and nothing a module sees. The template declares none of",
"the new members, so nothing it does changed meaning; module.json's coreApi",
"becomes ^1.19.0 for the equality check. The real core run was repeated at",
"this ref, with the template as the only module in the directory.",
"",
"2026-10-23: moved from 0a37a44 (1.12.0) to 6890a74 (1.17.0, website#220,",
"the cutover that brought Steam sign-in and the Rust last fixes to `main`),",
"past 1.13.0 to 1.16.0, which reached `main` together on 2026-10-22. The",
"chapters were read again for all five versions:",
"- 1.13.0: templates (chapter 5 gains a section), a param's duration, a",
" lease's example, an option row's note;",
"- 1.14.0: a param's multiple and max;",
"- 1.15.0: a retry's retryAfterMs;",
"- 1.16.0: a dry run's detail.warnings;",
"- 1.17.0: onIdentityLinking / onIdentityLinked and ctx.identities.list",
" (chapter 2 gains a section, and its call count is corrected to nineteen).",
"Two core changes in the same cutover carried no version bump, and both are",
"taught: a site runs ONE game module, which the loader now enforces at boot",
"(chapter 1, the reader most likely to be surprised by it), and a resource's",
"`until` is read at reconcile (chapter 5). The template declares none of the",
"new members, so nothing it does changed meaning; module.json's coreApi",
"becomes ^1.17.0 for the equality check. The real core run was repeated at",
"this ref, with the template as the only module in the directory.",
"",
"2026-10-06: moved from 655fbf3f (1.10.0) to 0a37a44 (1.12.0, website#210),",
"past 1.11.0 (website#209), which this pin had skipped. Chapter 5 was read again",
"for both versions and gained two paragraphs:",
@@ -15,7 +71,7 @@
"The template declares neither, and nothing it does changed meaning. The real",
"core run below was repeated at this ref.",
"",
"Before that move:"
"Before that move:",
"This one is the EVENT SYSTEM cutover, the commit MODULE_API_VERSION 1.10.0",
"reached `main` on (website#199), and 1.10.0 is what template/module.json",
"declares. It moved here from 66bb3b9a (1.9.0, the engagement cutover) because",

View File

@@ -2,7 +2,7 @@
"id": "examplegame",
"name": "Example Game",
"version": "0.1.0",
"coreApi": "^1.12.0",
"coreApi": "^1.21.0",
"server": "server/index.js",
"client": { "entry": "client/dist/entry.js" },
"schema": "server/db/schema.sql",