From 3116e7bbf68a0f80b8ddb7d2ec3f8fd645ced475 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 12 Aug 2026 14:28:26 -0500 Subject: [PATCH] =?UTF-8?q?docs(modules):=20close=20Phase=205=20=E2=80=94?= =?UTF-8?q?=20the=20acceptance=20run,=20and=20the=20page=20shell=20it=20fo?= =?UTF-8?q?und?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 3 of Phase 5 (MODULE_SYSTEM.md 2.11.1), and the phase's last slice. docs/modules/kit-acceptance.md is decision 5's deliverable: a cold agent given the Integration Kit and the documents it links to — never core's source, never module-uo — built a working module for a second game, which was then installed into a real core and taken through MODULE_API.md 7.7's browser smoke. Verdict recorded whichever way it went, and it went **yes, with caveats**: one pass, no core source, and three of the four normative documents never opened. The finding that justifies the two-stage shape is the one the agent structurally could not reach, because it had no core to render against. A module page built exactly as the kit teaches renders OUTSIDE the site: PublicLayout supplies the chrome and not the body, and the `shell-... page-body` wrapper every core public page writes for itself is two class names that appear in no contract. That is 3.4's own stated failure — "a module page that does not look like the site it is installed in" — reached by following 3.4. Fixed in core rather than documented at the reader, so the class names stay core's private business and the theming workstream keeps its freedom to rename them: PublicLayout takes an opt-in `shell` width, MODULE_API_VERSION 1.5.0 (website#148, merges first). - MODULE_API.md 1.1: 1.5.0's entry, and a new bump-table row — adding an OPTIONAL prop or argument is minor. "A member's signature changes" is major because a call already written changes meaning, and an optional prop changes none; the table now says what it means rather than leaving it to be argued. - MODULE_API.md 3.4: the shell prop, why a module names a width and never a class, and the eight-vs-seven miscount the run also turned up — the kit had faithfully carried it out of the contract into the template, which is the never-re-specify rule working exactly as designed on a wrong input. - rust-dryrun.md: coreApi ^1.3.0 -> ^1.5.0, as a dated correction per decision 33. It is the only complete module.json in the kit's reading path and nothing checks a JSON block inside a Markdown file, which is the reusable half. - MODULE_SYSTEM.md 2.11.1: slice 3 recorded, plus the third finding worth generalising — a check whose failure message asserts a diagnosis has to be right about it. `check:swagger` failed on a pristine template on Windows (CRLF) while blaming the routes, green on the Linux runner forever. - Decision 34: core owns the page body as well as the chrome. The banner does not come off. Decision 32 makes that a person's to remove, this run exercised the website-module half only (the module has no sidecar, so chapters 3 and 4 were never tested), and an agent does not skim or give up. Co-Authored-By: Claude --- modules/kit-acceptance.md | 139 ++++++++++++++++++++++++++++++++++++++ modules/rust-dryrun.md | 13 +++- website/MODULE_API.md | 62 ++++++++++++++--- website/MODULE_SYSTEM.md | 46 ++++++++++++- 4 files changed, 248 insertions(+), 12 deletions(-) create mode 100644 modules/kit-acceptance.md diff --git a/modules/kit-acceptance.md b/modules/kit-acceptance.md new file mode 100644 index 0000000..282a9ae --- /dev/null +++ b/modules/kit-acceptance.md @@ -0,0 +1,139 @@ +# Integration Kit — the acceptance run + +**Phase 5's fifth decision** ([`../website/MODULE_SYSTEM.md`](../website/MODULE_SYSTEM.md) §2.11.1 d5). +The [Integration Kit](https://gitea.whitlocktech.com/RunicGateway/Integration-kit) is finished when +someone outside this project builds a working module for a new game by following it alone. That +cannot be manufactured, so this is the **proxy**: a cold agent given the kit and the documents it +links to — never core's source, never `module-uo` — asked to build a module for a second game, whose +result was then installed into a real core and loaded in a browser. + +Run **2026-08-12**, against `Integration-kit` at slice 2 (PR #3) and core at website `edge` 1b692bf. + +> The verdict below does **not** clear the draft banner. Decision 32 makes that a human's to remove, +> and this exercise is aimed at the same target with a machine. It is worth what it found, not more. + +--- + +## 1. Verdict + +> **Yes, with caveats.** A competent developer who had never seen this platform can build a working +> module from this kit alone. + +It did, in one pass, without reading core's source and **without opening three of the four normative +documents** — `MODULE_SYSTEM.md` and both `link/` specs were never touched, correctly, since the +deliverable was a website module with no sidecar. `MODULE_API.md` was opened only at the end, to +audit the kit rather than to build against it. + +Its own summary of what carried the run: + +> What raises this to "yes" rather than "yes but barely" is the template. The four suites plus the +> two guard scripts constitute a machine-checkable specification of a valid module, and they are +> better documentation than any chapter could be — every rule they enforce comes with a comment +> explaining the bug that motivated it and how it looked when it went wrong. + +And the caveat that mattered most, which is a process one rather than a comprehension one: **the +first command the kit tells a reader to run fails on a pristine copy** (F1). + +## 2. Method, and the one thing it could not enforce + +| | | +| --- | --- | +| **Given** | `Integration-kit` at slice 2, tracked files only (so the template's chunk had to be built, as a reader's would); local mirrors of the five documents the kit links out to, at the same relative paths, because the kit's links point at a Gitea host a credential-less agent cannot reach | +| **Denied** | core's source, `module-uo`, the web beyond the npm registry | +| **Asked for** | a trivial but real module for **Rust** — valid `module.json`, a public route backed by its own table, a prebuilt chunk rendering it, a purge fragment, and whatever guards the kit says a module needs, passing | +| **Told to log** | findings *as they happened*, into a running file, on the grounds that being stuck is worth more than remembering being stuck | +| **Model** | Claude Opus 5 | + +Rust was chosen deliberately over an unfamiliar game: it is the subject of +[`rust-dryrun.md`](rust-dryrun.md), which the kit's front page names as the first thing to read, so +the run also measures whether that document is followable. + +**The honest limitation.** A subagent on this machine inherits the workspace as its shell's default +directory, so isolation is a clean tree plus explicit instructions plus a self-reported list of every +path read — not a sandbox. The agent reported no read outside its root and disclosed one unrelated +process slip unprompted, which is some evidence of good faith but is not proof. A future run should +put the agent somewhere it *cannot* reach core. + +## 3. What was built, and what it proved + +`module-rust` — id `rust`, `coreApi ^1.4.0`, two public routes over two tables (`rust_servers` + +`rust_wipes`, FK, child-first purge), a 8.0 kB chunk with two pages and a nav row. **44 server + 18 +client tests, none skipped**; `check:imports`, `check:swagger`, `check:externals` all green. + +Installed into a real core on `edge` and booted **against an empty database** — the condition Phase 4 +slice 3 established as the only one that can see a schema-ordering defect: + +- loader scanned it, schema fragment replayed (4 statements), `onBoot` ran, state `started` +- `/api/v1/public/modules` published it; both routes served correct JSON +- the chunk registered under enforced `script-src 'self'` with **zero CSP reports and zero console + errors**, and a **deep-linked param route rendered on a cold load** — the `readyState` ordering + path [`../website/MODULE_API.md`](../website/MODULE_API.md) §7.7 exists for + +Two things it did *not* do are worth recording as evidence the contract held for a stranger: it never +reached into core (`check:imports` green), and it wrote no connection to a game server — in fact it +wrote a test asserting that about itself, having read in chapter 2 that §2.7 is the one rule with no +CI behind it. + +## 4. Findings + +Eight from the agent, one from the browser. **B1 could not have been found by the agent** — it had no +core to render against, which is the structural gap this two-stage method exists to cover, and it is +the most valuable single result of the run. + +| # | Class | Where | What | Disposition | +| --- | --- | --- | --- | --- | +| **B1** | **WRONG** | `MODULE_API.md` §3.4 | A module page built exactly as the kit teaches **renders outside the site**. `PublicLayout` gives the chrome, not the body; core's nine public pages each wrap content in `shell-… page-body`, whose class names appear in no contract. Content at x=0, no padding, footer riding up under it | **Fixed** — `PublicLayout` takes an opt-in `shell` prop, `MODULE_API_VERSION` **1.5.0** (website#148); §3.4 rewritten; template and chapter 2 updated | +| F1 | WRONG | `template/README.md`, `book/02` | `npm run check:swagger` **fails on a pristine, unedited copy** on Windows: the fragment compares byte-for-byte and a default Windows clone is CRLF. The message asserts a false cause — "the routes or their annotations changed" | **Fixed** — `template/.gitattributes` (`eol=lf`) *and* the comparison normalises line endings | +| F2 | WRONG | `rust-dryrun.md` §1 | `coreApi: "^1.3.0"` while everything else said 1.4.0 — in the only complete `module.json` in the kit's reading path | **Fixed** — `^1.5.0`, as a dated correction | +| F3 | MISSING | `template/README.md` | The rename checklist claims to name every file carrying a placeholder. Both `release.yml` flavours carry `gitea.example.com` and `your-org/your-module` under a literal `# CHANGE THESE` and are absent — and `checkRenameSites.js`'s pattern cannot match them, so CI is silent **by construction** | **Fixed** — rows added, pattern widened | +| F4 | FRICTION | `template/.gitea/workflows/release.yml` | The bundle's include list is hardcoded (`for d in boot.js core.js index.js db model router`). Add any top-level directory under `server/` and it is **silently dropped from every release**; the post-check only resolves the five paths in `module.json` | **Fixed** — inverted to an exclusion list | +| F5 | AMBIGUOUS | `world.router.js`, `book/02` | The comment warns that a backtick in a single-quoted `#swagger` description ends the string early — but the same file uses two backtick spans and they survive verbatim | **Fixed**, and sharpened: see below | +| F6 | AMBIGUOUS | `template/` | No `.gitignore`. The kit's own covers the template's paths, but a reader who copies the directory and runs `git init` inherits nothing — `node_modules/` included | **Fixed** — `template/.gitignore` ships | +| F7 | WRONG | `MODULE_API.md` §3.4 | "The kit is those **seven** members" over a table publishing **eight** exports (five rows; `PageState` contributes three). Propagated faithfully into the template's comment and core's `shared.js` — the kit's never-re-specify discipline carrying a miscount *out* of the contract | **Fixed** in all three | +| F8 | note | — | Three of the four normative documents were never opened, and never needed to be | Recorded above | + +### F5 is worse than reported, and the correction is the useful part + +Measured directly rather than reasoned about. A **backtick** inside a single-quoted `#swagger` +description is harmless — it survives verbatim into the fragment. A **double quote** is not, and it +does not fail the way the comment predicts: + +``` +// #swagger.summary = 'A "quoted" world status' + → "summary": "A \"" ← silently truncated + → swagger-autogen prints Success, and swaggerFragment.js's error capture sees nothing +``` + +The template told a reader that the tool would tell them. For the one case that actually bites, it +does not: the annotation is truncated, the generator reports success, and the only signal is a +`check:swagger` diff whose message blames the routes. **A chapter that predicts the wrong debugging +heuristic is worse than one that predicts none** (§2.11.1) — restated here for a comment inside the +template, which no check in that repo can see either. + +### Two things the run recommends that are not defects + +- **"Run every check on the untouched copy before you edit anything."** The agent did this off its + own bat, and it is what found F1; without a baseline it would have blamed its own edits. Chapter 1 + now says so. +- **The template should ship the §2.7 self-check.** Chapter 2 correctly says the no-game-connection + rule has no CI behind it, because an outbound socket is not statically detectable *in general*. But + a module can make a decidable claim about **itself**, and the agent wrote that check, with a + positive control (it catches a real RCON dial) and a negative one (prose about RCON does not trip + it). It called it "the check I would most want the template to ship", noting that for Rust + `new WebSocket(rconUrl)` in `boot.js` is ten lines away. Adopted into the template, from its code. + +## 5. What this does and does not settle + +**Settles:** the kit is sufficient for the website-module half of the job. Someone can start from +nothing and finish with a module that loads into a real core, serves, renders in the site's chrome, +and passes every rule the contract has a check for — without reading core. + +**Does not settle:** the sidecar and the game-plugin halves. This module has neither, so chapters 3 +and 4 were never exercised and remain proved only by the fact that they describe systems that exist +(`link/`, `servuo-plugins/`). Nor does it settle anything about a *person's* experience — an agent +does not get frustrated, does not skim, and does not give up, which are three of the failure modes +the banner is really about. + +**So the banner stays.** The next run of this exercise should either be a person, or an agent placed +somewhere it genuinely cannot reach core's source, and should be asked for a module **with** a +sidecar. diff --git a/modules/rust-dryrun.md b/modules/rust-dryrun.md index 39f3448..27b2359 100644 --- a/modules/rust-dryrun.md +++ b/modules/rust-dryrun.md @@ -27,7 +27,7 @@ in. If the contract survives that, "game-agnostic" means something. "id": "rust", "name": "Rust", "version": "0.1.0", - "coreApi": "^1.3.0", + "coreApi": "^1.5.0", "server": "server/index.js", "client": { "entry": "client/dist/entry.js" }, "schema": "server/db/schema.sql", @@ -42,6 +42,17 @@ in. If the contract survives that, "game-agnostic" means something. } ``` +> **Correction, 2026-08-12.** `coreApi` read `^1.3.0` here until the Integration Kit's acceptance run +> ([`kit-acceptance.md`](kit-acceptance.md)) found it. The contract was at 1.3.0 when this design was +> written and has moved twice since; the number is now `^1.5.0`. Left as a correction rather than a +> silent edit because *why* it went stale is the reusable part: this is the only complete `module.json` +> in the kit's reading path, so it is what a newcomer copies — and unlike the template, which CI holds +> against core's `MODULE_API_VERSION` on every pull request +> ([`../website/MODULE_SYSTEM.md`](../website/MODULE_SYSTEM.md) §2.11.1 d2), **a JSON block inside a +> Markdown document has nothing checking it.** A range is also the shape least likely to be noticed +> when it rots: `^1.3.0` is *satisfied* by a 1.5.0 core, so a module copied from here would have loaded +> fine and simply been wrong about what it was written against. + One prefix per tier, named for the module rather than for a feature — the opposite of module-uo's `/shard` + `/atlas` + `/uo-link`, and the better choice for anything new. Module-uo's prefixes are what they are because §1.2 froze the URLs core already served; a module written today has no such diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 988df6e..c26d0a1 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -26,13 +26,27 @@ here extends the contract first, in this file, before the module is written agai Core exports a single integer-major semver string from `server/src/modules/version.js`: ```js -const MODULE_API_VERSION = '1.4.0' +const MODULE_API_VERSION = '1.5.0' ``` The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first module chunk evaluates, which is earlier than any network round trip could answer. +**1.5.0 — Phase 5 slice 3, the page shell.** `PublicLayout` takes an optional **`shell`** prop — +`'narrow'`, `'mid'` or `'wide'` — that renders the page-body wrapper core's own pages have always +written by hand (§3.4). Found by the acceptance run in +[`../modules/kit-acceptance.md`](../modules/kit-acceptance.md): a module built by following the kit +alone rendered *outside* the site's page column, because the wrapper's two class names belong to +`theme.css` and appear in no contract. Omitting `shell` is 1.4.0's behaviour exactly, so core's own +pages are untouched. + +**Minor, and the table below is why that needs saying.** "A member's signature changes" is a major +bump, and a prop is a signature — but the rule is about a call that *already exists* changing +meaning, and an optional prop changes none. Read the table as being about what breaks, not about what +is typed: adding an optional argument is an addition, and `module-uo`'s `coreApi: "^1.3.0"` still +resolves. + **1.4.0 — Phase 5, the sidecar rule.** §2.7 gained one prohibition: a module does not open a connection to a game server from the website process. It talks to a **sidecar**, which owns the durable copy of the game's state. No member was added, removed or changed — the surface is identical @@ -68,6 +82,7 @@ requires a line of module code, and a mismatch fails that module loudly into `st | Change | Bump | | --- | --- | | A member is added to `ctx`, or a new `register*` call appears | minor | +| An **optional** prop or argument is added to an existing member | minor | | A member is removed or its signature changes | major | | Behaviour of an existing member changes without a signature change | major | | A core-internal refactor behind an unchanged member | none | @@ -779,21 +794,52 @@ The kit is **curated and closed**, not a re-export of `components/`: | Export | From | Why it is in the kit | | --- | --- | --- | -| `PublicLayout` | `components/PublicLayout.jsx` | the public chrome; a module page without it is a bare page | +| `PublicLayout` | `components/PublicLayout.jsx` | the public chrome **and**, via `shell`, the page body; a module page without it is a bare page | | `PageHeader` | `components/PageHeader.jsx` | title/subtitle furniture | | `Loading`, `ErrorState`, `EmptyState` | `components/PageState.jsx` | the three states every data page has | | `useAsync` | `lib/useAsync.js` | the fetch/loading/error hook every data page uses | | `useAuth`, `useSite` | `contexts/*` | read-only access to session and site settings | Everything else — tables, chips, tabs, the tiptap editor, dnd-kit — a module bundles itself. -Adding to the kit is a **minor** `MODULE_API_VERSION` bump; changing a kit component's props is a -**major** one. That is a real constraint on core and it is the price of the boundary being worth +Adding to the kit is a **minor** `MODULE_API_VERSION` bump; *changing* a kit component's existing +props is a **major** one, because that breaks a call already written. Adding an **optional** prop is +minor (§1.1). That is a real constraint on core and it is the price of the boundary being worth anything. -The kit is those **seven** members. An earlier draft of this table listed an eighth, `AdminPage`, and -core has no such component — admin views are plain markup inside `AdminLayout`. It was struck in -Phase 2 PR 7 rather than satisfied by inventing a core component with no consumer until Phase 3; -adding it later costs a minor bump, which is the case this versioning exists for. +The kit is those **eight exports** — five rows, because `PageState` contributes three. An earlier +draft of this table listed a ninth, `AdminPage`, and core has no such component — admin views are +plain markup inside `AdminLayout`. It was struck in Phase 2 PR 7 rather than satisfied by inventing a +core component with no consumer until Phase 3; adding it later costs a minor bump, which is the case +this versioning exists for. + +#### `PublicLayout` gives you the chrome; `shell` gives you the body + +`PublicLayout` renders the header, the footer and the `.page` flex column. It does **not**, by +default, render the body wrapper that every core public page writes for itself: + +```jsx + // 'narrow' (760px) · 'mid' (880px) · 'wide' (1280px) + + … + +``` + +Without a `shell`, content renders full-bleed from x=0 with no vertical padding, and the footer rides +up underneath it instead of sitting at the bottom of the viewport. That last part is the one worth +knowing, because it looks like a CSS bug in the module rather than a missing wrapper: the footer is +pushed down by `flex: 1` on the body element, so a page with no body element has nothing pushing it. + +**Name a width, never a class.** The classes those widths map to are `theme.css`'s and are *not* +contract — the theming workstream owns that file and must stay free to rename them. A module that +hardcoded `className="shell-narrow page-body"` would look right today and break silently on a rename, +with nothing failing anywhere. An unrecognised width falls back to `narrow` rather than to no +wrapper at all, because a page at the wrong width still looks like the site and a page with no +wrapper does not. + +This exists because the Integration Kit's acceptance run +([`../modules/kit-acceptance.md`](../modules/kit-acceptance.md)) proved a module author cannot +discover the wrapper: the kit hands them `PublicLayout` and describes it as the public chrome, which +is true and was not enough. Core's own pages keep writing their wrapper by hand and are unaffected. ### 3.5 `api` — the request primitive diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index f7888cd..7460a49 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -2088,9 +2088,48 @@ the book's code blocks are quotations with a path next to them rather than prose like code. **Progress:** slice 0 landed as `Integration-kit`#1, slice 1 as #2 (45 files, module id -`examplegame`, pin moved to website `edge` 1b692bf so the `coreApi` equality assertion arms), and -slice 2 as #3 — the four chapters, plus `checkChapterPaths.js` (d2 above). Only slice 3, the -acceptance run of decision 5, is outstanding. +`examplegame`, pin moved to website `edge` 1b692bf so the `coreApi` equality assertion arms), slice 2 +as #3 — the four chapters, plus `checkChapterPaths.js` (d2 above) — and **slice 3 closes the phase**. + +##### Slice 3 — the acceptance run, and what it cost core + +The run and its findings are [`../modules/kit-acceptance.md`](../modules/kit-acceptance.md), as d5 +requires, verdict included: **yes, with caveats.** A cold agent given the kit and the documents it +links to — never core's source, never `module-uo` — built a working `module-rust` in one pass, +*without opening three of the four normative documents*. It was then installed into a real core on +`edge` and taken through §7.7's browser smoke. + +**The finding that justifies the two-stage shape: the agent could not have found the worst defect, +because it had no core to render against.** A module page built exactly as the kit teaches renders +*outside the site* — `PublicLayout` supplies the chrome and not the body, and the `shell-… page-body` +wrapper every core public page writes for itself is two class names that appear in no contract. That +is [`MODULE_API.md`](MODULE_API.md) §3.4's own stated failure ("a module page that does not look like +the site it is installed in") reached by following §3.4. Fixed in core rather than documented at the +reader: `PublicLayout` takes an opt-in `shell` prop, **`MODULE_API_VERSION` 1.5.0** (website#148), +which makes the class names core's business again — the theming workstream owns `theme.css` and a +rename would otherwise break every module's look with nothing failing. + +**Second: a kit is not exempt from the thing it is written to prevent.** §2.11's rule is that the kit +never re-specifies a contract, and it held — the agent audited the kit against `MODULE_API.md` +afterwards and found no divergence. But the contract itself said the UI kit was "seven" members over +a table publishing eight, and the kit had faithfully carried that miscount *out*. Fidelity propagates +errors as well as truth; the check for that is a reader, which is what this exercise bought. + +**Third, and cheapest to act on: the first command the kit tells a reader to run failed on a pristine +copy.** `check:swagger` compares the committed fragment byte-for-byte, a default Windows clone is +CRLF, and the failure message names a cause that is false ("the routes or their annotations +changed"). Green on the Linux runner forever. The lesson generalises past line endings — **a check +whose failure message asserts a diagnosis has to be right about it**, because a reader who is twenty +minutes in will believe it over their own eyes. + +Six other findings, all small, all fixed in the same pass; two recommendations adopted (chapter 1 now +says to run every check on the untouched copy first, which is what found the CRLF defect; and the +template ships the §2.7 self-check the agent wrote for itself — the rule has no CI *in general*, but +a module can make a decidable claim about its own tree). + +**The banner does not come off.** d5 makes that a person's to remove and d32 says so; this run +exercised the website-module half only — the module has no sidecar, so chapters 3 and 4 were never +tested — and an agent does not skim, get frustrated, or give up. **Slice 2's own verification is the shape decision 5 will scale up.** Chapters 1 and 2 quote a tree CI builds, so what remained unproven was the *walkthrough*: whether following chapter 1 produces what @@ -2144,3 +2183,4 @@ check in this repo can see. | 31 | That is **contract, not advice** — API §2.7, `MODULE_API_VERSION` 1.4.0, normative prose with no CI behind it | API §1.1, §2.7 | | 32 | The kit is a **draft on its own front page** until a person outside the org passes its acceptance; a cold-agent run is the interim proxy | §2.11.1 d5 | | 33 | `rust-dryrun.md` is **corrected with a dated note**, not silently rewritten — a dry run's value is the record of what it found | §2.11.1 d6 | +| 34 | Core owns the page **body** as well as the chrome: `PublicLayout` takes a `shell` width, and its CSS class names stay core's private business | API §3.4, §2.11.1 slice 3 |