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 |