diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 68c1d24..aed2c49 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -817,6 +817,25 @@ specifiers in the client bundle are the four declared externals. A hit fails the Phase 3's acceptance criterion 1: no `shard`, `uoLink`, `cliloc`, `atlas` or `towncrier` outside `modules/`, as a CI grep test rather than a review promise. +**Settled 2026-08-11: the grep reads code, not prose.** It covers four things, and each of them is a +thing a module owns: + +1. **File and directory names** under `server/src/`, `server/scripts/`, `server/db/` and `client/src/`. +2. **Import and require specifiers** — the path in `require('…')` / `from '…'`. +3. **Route path literals** — the string arguments to `.get`/`.post`/`.put`/`.patch`/`.delete`/`.use`. +4. **Declared identifiers** — function, const, class and property names. + +It does **not** read comments or string content generally, and that is not a loophole. Core's +marketing copy legitimately says "shard" — `About.jsx`, `Screenshots.jsx`, `SiteFooter.jsx`, +`heroLayout.js` — and a literal word grep would turn each of those into a CI failure while proving +nothing about the boundary. Worse, it would forbid a core comment from ever using the word as an +example, which is the sort of rule people work around rather than obey. The boundary this test exists +to defend is *structural*: core must not name a module's files, import them, route to them, or +declare their symbols. It can talk about them in English. + +Core's UO-flavoured default copy is dealt with directly instead, as `MODULE_SYSTEM.md` §2.7.1's +slice 8 — a rewrite with its own review, not an exemption. + ### 5.3 Zero-line route manifest diff (CI, both repos) `npm run routes:manifest -- --check` in core; the module generates and freezes its own manifest in @@ -824,6 +843,16 @@ its own repo, using the same script pointed at a core+module app. Phase 2 must p diff in core's; Phase 3 moves the UO entries out of core's and into module-uo's, which is the one diff the whole workstream is allowed. +**Settled 2026-08-11: `module-uo`'s CI checks core out at a pinned ref.** The module's workflow +clones `RunicGateway/website` at a ref recorded in the module repo, drops itself in as `modules/uo`, +and runs core's own `routeManifest.js`. Nothing else proves the URLs a module claims are the URLs it +actually serves — a manifest frozen by hand goes stale silently, and the failure it would have caught +is a route that moved. + +Pinning the ref rather than tracking `edge` is what keeps this from being a source of unexplained red +Xes: core moves for reasons that have nothing to do with the module, and a bump is then a deliberate +commit that says which core the module was last proved against. + --- ## Part 6 — Amendments to MODULE_SYSTEM.md diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index 5440d57..889aa6f 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -746,7 +746,60 @@ Acceptance, all four required: ownership move, which changes no URL. After extraction the core manifest no longer contains UO routes — `module-uo` generates and freezes its own in its own repo. 4. **A written `module-rust` dry run** — manifest, mounts, nav entries, one notification stream — not - implemented, to prove the contract generalises before more is built on it. + implemented, to prove the contract generalises before more is built on it. It lands as + `docs/modules/rust-dryrun.md`, where §2.10 already aggregates module documentation; Phase 5's + Integration Kit links to it rather than copying it, per the kit's own never-re-specify rule + (§2.11). + +#### 2.7.1 Phase 3's shape — settled 2026-08-11 + +Measured against `edge` at the close of Phase 2, the surface is **72 server files / ~9,700 lines**, +**51 client files / ~3,700 lines**, and **32 of core's 82 server test files**. The counts in the +paragraph above were written in Phase 0 against a smaller tree and are superseded by the slice table +below. + +**The finding that sets the order: the two halves are independent.** Because §1.2 preserves API URLs +exactly, core's client keeps calling `/api/v1/public/shard/status` after that route is served by the +module, and a module page calls the same URL while core still serves it. Nothing forces a feature's +server and client halves to move together, so the extraction is **server-first, then client**, sliced +by feature — which keeps each PR inside one layer and one review's worth of context. + +**Merge order across the two repos: `module-uo` first, then `website`.** The loader's `ownedByCore` +probe means a module cannot *load* while core still owns its prefix — but `module-uo`'s own CI never +loads it into core, so its PR merges perfectly well beforehand. Taking that order means `edge` serves +the feature from core right up to the moment core drops it, and there is never a window where the +branch is missing a feature outright. The reverse order would break `edge` at every slice boundary +for the length of a review. Verification is unaffected either way: a slice is proved by running the +*pair* together locally — the module branch checked out into `website/modules/uo`, the deletion +branch checked out in `website/` — before either merges. + +Each slice is one `module-uo` PR (adds), one `website` PR (deletes), and one `docs` PR: + +| # | Slice | Moves | +| --- | --- | --- | +| 0 | **The bundle skeleton** | `module.json`, both `package.json`s, `server/index.js` registering nothing, the Vite library build + the four shared-dep shims, an empty `schema.sql`/`purge.sql`, CI armed, the §5.1 zero-internal-imports check. `website` untouched. | +| 1 | **Atlas + clilocs** | `shardAtlas`/`shardClilocs` models, `clilocParse`/`clilocSource`/`spawnAtlasParse`/`spawnAtlasSource`, `public/atlas.*`, `admin/shardAtlas.controller`/`shardClilocs.controller`, `scripts/importSpawnAtlas.js`, the art JSON | +| 2 | **The live shard** | `shardState`/`shardEvents`/`shardVisibility`/`uoLinkConfig` models, `uoLinkClient`/`uoLinkSocket`/`shardIngest`/`shardBroadcast`/`shardPush`/`shardVisibility` utils, `config/shardStreams.js`, `public/shard.*`, `admin/shard.router`/`shardOps`/`shardVisibility`/`uoLink.*` | +| 3 | **Market** | `shardMarket` model, `shardSales` util and their routes | +| 4 | **Account links + the extension slot** | `shardLinks` model, `player/shard.*`, `admin/usersShard.*` minus `getUser` — the first real filling of `admin.users.detail` | +| 5 | **The news leg** | `newsGump.js`, `shardAnnounce.js`, the town-crier `registerAnnounceLeg` | +| 6 | **Public pages** | `Shard`, `ShardActivity`, `Rules`, `Atlas`, `AtlasCreature`, `ChampSpawns`, `Market`, `MarketVendor`, `Governors`, `Guilds`, `Houses`, `Leaderboards`, `PlayersOnline`, `VendorSales`, `data/cityCrests.js`, `lib/shardEvents.js`, `lib/useShardFeed.js`, and their public nav rows — under `/uo/*` per §2.8 | +| 7 | **Admin + player pages** | `ShardAdmin`, `ShardOps`, `ShardVisibility`, `SpawnAtlas`, `HousesAdmin`, `AdminCharacter(s)`, `PlayerCharacter(s)`, `GameAccounts`, `CharacterSheet`, `CharacterStats`, `ShardAccountActions`, `CreateGameAccountForm`, `useShardFeatures` and the `useShardFlags` feature provider — under `/admin/uo/*` and `/player/uo/*` | +| 8 | **De-UO core's copy** | `About`, `Screenshots`, `Website`, `SiteFooter`, `heroLayout`'s defaults, `api/client.js`'s `shard`/`atlas` namespaces, and the comments in `navOverrides.js` — plus the §5.2 CI grep that keeps them out | +| 9 | **Close the phase** | `module-uo`'s frozen route manifest and release workflow; `docs/modules/uo/` and `docs/modules/rust-dryrun.md` | + +**Criterion 1 is a grep over code, not over prose** — see [API §5.2](MODULE_API.md#52-zero-uo-identifiers-in-core-ci-website-repo) +for what that means precisely. Core's marketing copy says "shard" in a dozen places, and a literal +word grep would have made every one of them a CI failure while proving nothing about the boundary. +Slice 8 rewrites that copy anyway, because a core that still reads as a UO site is not the +game-agnostic platform this workstream is for — but it is a deliberate piece of work with its own +review, not an exemption hidden in a grep pattern. + +**One small gap in the kit, deliberately not closed.** The UO client views import almost exactly the +seven §3.4 members — plus `lib/format.js`, a pure leaf formatter. The module **vendors a copy** +rather than core adding an eighth member: the kit is closed on purpose, and a function with no +props and no layout cannot drift the way a component can. The same is not true of `PublicLayout`, +which is why that one is in the kit. **Phase 4 — Delivery.** The admin-panel Modules screen (install, enable, disable, retry, purge, `startup_failed` with its recorded reason) and the Docker-environment path from §2.5. Deliberately @@ -896,3 +949,7 @@ row for it — when it has content, not while it is an empty repo. | 11 | Website work lands on `edge` and reaches `main` as one cutover at the end | §2.9 | | 12 | The module repo is `RunicGateway/Module-uo`; the module id is `uo` | §2.3 | | 13 | `RunicGateway/Integration-kit` is the module-builder's instruction book — module + sidecar + game plugin, teaching only, never re-specifying a contract | §2.11 | +| 14 | Phase 3 extracts **server-first, then client**, sliced by feature; `module-uo` merges before `website` in each pair | §2.7.1 | +| 15 | Criterion 1's grep reads **code, not prose**; core's UO copy is rewritten in its own slice instead | API §5.2, §2.7.1 | +| 16 | `module-uo`'s CI checks core out at a **pinned ref** to generate its frozen route manifest | API §5.3 | +| 17 | The `module-rust` dry run lands as `docs/modules/rust-dryrun.md`; the Integration Kit links to it | §2.7.1, §2.11 |