refactor(modules)!: de-UO core's copy, and enforce it (phase 3, slice 4)
All checks were successful
PR Checks / bot-install (pull_request) Successful in 18s
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / server-tests (pull_request) Successful in 31s

Phase 3's acceptance criterion 1, made real. Three things, one review:

**The dead bindings.** `client/src/api/client.js` still carried ~190 lines of UO
namespaces — `shard`, `atlas`, the two SSE URLs, `admin.shard/shardOps/atlas/
userShard`, the uo-link and town-crier calls, `player.shard` — with zero core
consumers since slice 3 deleted the views. module-uo vendors its own bindings.
The five assertions core's `apiClient.test.js` made about those URLs moved with
them (Module-uo#5); the encoding test that used `governorHistory` now uses a
core route.

**The copy.** Core is the platform, not one game's site, so its words are
game-neutral now: `About`, `Screenshots`, `Website`'s cards, `Status` (which was
never about a game server at all — it reports site mode), `Wiki`, `SiteFooter`,
the default hero, `brand.js`'s tagline and description, the seeded wiki
categories, and two user-visible NavEditor strings that named a module's admin
screen by its proper name. Which game an instance is for is the operator's to
say — BRAND_* vars, the hero editor, CMS pages — and every real instance already
does: `.env.uomysticmoon.example` sets both brand strings explicitly, so nothing
live changes wording. Wiki page SLUGS are untouched: `seedDefault*` only inserts
what is absent, so renaming one adds a duplicate page to every install.

Also gone: an orphan comment block in `schema.sql` describing the spawn-atlas
tables slice 1 took away, and the two settings rows core seeded for a module
(`game_account_signup`, `uo_link_protocol_3_migrated`). The second was a live
defect — see Module-uo#5, which takes ownership of both and repairs the
one-shot migration core's ordering had disabled.

**The check.** `scripts/checkModuleIdentifiers.js` + `npm run check:modules`,
first step of the server-tests job because it needs no dependencies. It reads
CODE, not prose — file names, import specifiers, route path literals, declared
identifiers and property names — per §5.2, so core's English may still say
"shard" where saying it is worth more than the word costs.

Two things it gets right only because getting them wrong was tried first: it
matches WHOLE WORDS (a substring pass flags `defaultImage`, which contains
"ultIma", four times in this repo), and it strips comments and string bodies in
one character walk (a comment contains quotes, a string contains `//`) — the
`checkImports.js` lesson. It has its own 17-test suite, because a boundary check
that silently stops checking is worse than none. The three §6.5 grandfathering
allowlists are exempt by name, and an exemption that stops matching fails the
build rather than lingering.

BREAKING CHANGE: core no longer seeds `game_account_signup` or
`uo_link_protocol_3_migrated`; module-uo's schema fragment does. An install
running core without module-uo keeps whatever rows it already has and gains no
new ones — nothing in core reads either key.

Deferred to slice 5, deliberately: README.md's 48 UO mentions, including a
`## Shard integration (uo-link)` section and the architecture diagram. That is
documentation, which §5.2 does not cover, and it belongs with the phase-closing
docs pass rather than half-done here.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 21:41:18 -05:00
parent a99ead4ee4
commit 0c4eacfa4a
21 changed files with 551 additions and 289 deletions

View File

@@ -128,10 +128,10 @@ test('wiki() with no options sends no query string at all', async () => {
assert.equal(calls[0].url, '/api/v1/public/wiki')
})
test('path params are URL-encoded (a token/city with unsafe characters is escaped)', async () => {
test('path params are URL-encoded (a token with unsafe characters is escaped)', async () => {
willReply({ body: {} })
await api.shard.governorHistory('Serpents Hold', 5)
assert.match(calls[0].url, /\/governors\/Serpent%E2%80%99s%20Hold\/history\?limit=5/)
await api.getInvite('a b/c?d')
assert.equal(calls[0].url, '/api/v1/auth/invite/a%20b%2Fc%3Fd')
})
test('DELETE self-service session revoke encodes the id and uses the DELETE method', async () => {
@@ -140,44 +140,3 @@ test('DELETE self-service session revoke encodes the id and uses the DELETE meth
assert.equal(calls[0].opts.method, 'DELETE')
assert.match(calls[0].url, /\/auth\/me\/sessions\/a%20b%2Fc$/)
})
// ── spawn atlas (Protocol 3.0 Part C) ───────────────────────────────────
// The atlas lives at /public/atlas, NOT under /public/shard: it is static shard
// content parsed from the shard's own files, so it must not look sidecar-backed.
// Asserted here because the split is a design decision, not an accident of
// spelling.
test('atlas reads hit /public/atlas, not /public/shard', async () => {
willReply({ body: { creatures: [] } })
await api.atlas.creatures()
assert.equal(calls[0].url, '/api/v1/public/atlas/creatures')
})
test('atlas.creatures() sends only the filters that are set', async () => {
willReply({ body: { creatures: [] } })
await api.atlas.creatures({ q: 'lizard man', facet: 'Ter Mur', limit: 25 })
const url = new URL(calls[0].url, 'http://x')
assert.equal(url.pathname, '/api/v1/public/atlas/creatures')
assert.equal(url.searchParams.get('q'), 'lizard man')
assert.equal(url.searchParams.get('facet'), 'Ter Mur')
assert.equal(url.searchParams.get('limit'), '25')
assert.equal(url.searchParams.get('offset'), null) // 0 is not sent
})
test('atlas.creature() encodes the slug and carries the facet filter through', async () => {
willReply({ body: {} })
await api.atlas.creature('lizardman/rare', { facet: 'Felucca' })
assert.match(calls[0].url, /\/public\/atlas\/creatures\/lizardman%2Frare\?facet=Felucca$/)
})
test('admin atlas actions use the right methods and bodies', async () => {
willReply({ body: {} })
await api.admin.atlas.import(true)
assert.equal(calls[0].url, '/api/v1/admin/shard/atlas/import')
assert.equal(calls[0].opts.method, 'POST')
assert.equal(calls[0].opts.body, JSON.stringify({ force: true }))
willReply({ body: {} })
await api.admin.atlas.setPath('/srv/servuo')
assert.equal(calls[1].opts.method, 'PUT')
assert.equal(calls[1].opts.body, JSON.stringify({ path: '/srv/servuo' }))
})