spike(modules): carry /public/atlas/* behind the proposed module surface

THROWAWAY BRANCH — evidence for the Phase 1 contract, never merged. See
modules/uo/SPIKE.md and docs/website/MODULE_API.md Part 7.

The six public spawn-atlas routes now live in modules/uo/, reached only through
the ctx/register surface, with the client half loading as a prebuilt ESM chunk.
All three exit criteria met:

  • zero internal-file imports from the module into core; the built chunk has
    zero bare import specifiers and bundles no React
  • routes.manifest.json AND routes.guards.json are byte-identical
  • /uo/atlas renders from /modules/uo/entry.js under script-src 'self' with
    zero CSP violation reports

729 core tests and 81 module tests pass. Verified end to end against the real
database: the schema fragment replays after core's, onBoot runs the atlas
refresh, and the six API URLs answer unchanged.

Two things the spike changed in the contract:

  • ctx.express / ctx.validator. A module lives outside server/, so Node never
    reaches server/node_modules and require('express') fails outright — the
    server-side twin of the one-React rule, which §2.6 had only for the client.
  • window.__rg.jsxRuntime, so a module can build with the automatic JSX
    runtime its tooling already assumes rather than being forced to classic.

And it confirmed §6.1 empirically: regenerating the OpenAPI spec silently
deleted all 361 lines of the atlas paths with "Swagger-autogen: Success", while
the route manifest kept all six in the same run. That is exactly the
static-analysis-vs-runtime split the fragment merge exists to prevent.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-10 05:29:35 -05:00
parent f1dda8fe66
commit bf470c7658
55 changed files with 4638 additions and 601 deletions

View File

@@ -0,0 +1,134 @@
// ── Public: the spawn atlas ────────────────────────────────────────────────
//
// A browsable catalogue of what the shard CONTAINS — which creatures spawn,
// where, how many, and which champion altars are configured. Everything here is
// a plain indexed read of the tables the boot-time import fills from the shard's
// own ServUO tree (docs/website/SPAWN_ATLAS.md).
//
// Two properties separate this from /public/shard/*:
//
// • **Nothing touches the sidecar.** The atlas is static shard content, not
// live shard state, so these pages stay fully populated while the shard is
// down. That is why the routes are mounted at /public/atlas and are
// siteMode-gated like /posts and /wiki, rather than under /shard.
// • **The live champion feed is a different thing.** `/atlas/champions` is the
// configured roster ("there is an Unholy Terror altar in Deceit");
// `/shard/champs` is the running state ("it is on level 3 right now").
//
// Every response is still passed through `projectFeature` for the `atlas`
// feature. It declares no sensitive fields today, so the projection is a
// no-op — but v3.md §3.6.1's rule is that a read path returning shard data and
// not projecting is a bug, and the cost of honouring it is one call per handler
// rather than a retrofit the first time a field needs gating.
const atlas = require('../model/shardAtlas/shardAtlas.model')
const visibility = require('../utils/visibility')
const log = require('../core').logger('public-atlas')
const FEATURE = 'atlas'
// Query params arrive as strings; express-validator has already bounded them.
const int = (value, fallback) => {
const n = Number.parseInt(value, 10)
return Number.isFinite(n) ? n : fallback
}
const str = (value) => (typeof value === 'string' ? value.trim() : '')
// GET /public/atlas/creatures?q=&facet=&limit=&offset=
async function getCreatures(req, res) {
try {
const page = await atlas.searchCreatures({
q: str(req.query.q),
facet: str(req.query.facet),
limit: int(req.query.limit, 50),
offset: int(req.query.offset, 0),
})
return res.json(await visibility.project(FEATURE, page, req))
} catch (err) {
log.error('atlas.getCreatures', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// GET /public/atlas/creatures/:slug — one creature, with the places it spawns.
//
// 404 means "no such creature in this atlas", which also covers "the atlas has
// never been imported" — an empty atlas has no slugs, and there is nothing more
// specific to say to an anonymous caller.
async function getCreature(req, res) {
try {
const creature = await atlas.getCreature(req.params.slug, {
facet: str(req.query.facet),
points: int(req.query.points, 200),
})
if (!creature) return res.status(404).json({ message: 'Not Found' })
return res.json(await visibility.project(FEATURE, creature, req))
} catch (err) {
log.error('atlas.getCreature', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// GET /public/atlas/regions?facet=&q=
async function getRegions(req, res) {
try {
const regions = await atlas.listRegions({
facet: str(req.query.facet),
q: str(req.query.q),
})
return res.json(await visibility.project(FEATURE, regions, req))
} catch (err) {
log.error('atlas.getRegions', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// GET /public/atlas/landmarks?facet=&q=
async function getLandmarks(req, res) {
try {
const landmarks = await atlas.listLandmarks({
facet: str(req.query.facet),
q: str(req.query.q),
})
return res.json(await visibility.project(FEATURE, landmarks, req))
} catch (err) {
log.error('atlas.getLandmarks', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// GET /public/atlas/champions?facet= — the CONFIGURED altar roster.
async function getChampions(req, res) {
try {
const champions = await atlas.listChampions({ facet: str(req.query.facet) })
return res.json(await visibility.project(FEATURE, champions, req))
} catch (err) {
log.error('atlas.getChampions', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// GET /public/atlas/meta — what is loaded: facets, counts, when it was imported.
//
// Public-safe by construction: the model omits the ServUO path, the per-file
// hashes and the pending-refresh state, all of which describe the operator's
// filesystem rather than the game world. The admin status route carries those.
async function getMeta(req, res) {
try {
return res.json(await visibility.project(FEATURE, await atlas.publicMeta(), req))
} catch (err) {
log.error('atlas.getMeta', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
module.exports = {
getCreatures,
getCreature,
getRegions,
getLandmarks,
getChampions,
getMeta,
}

View File

@@ -0,0 +1,132 @@
// Public · Atlas — the spawn atlas / bestiary. Static shard CONTENT derived from
// the shard's own ServUO tree, not live shard state.
//
// Mounted at /api/v1/public/atlas by public/index.js. Two deliberate differences
// from the /public/shard routes next door (docs/link/v3.md §6):
//
// • **Not under /shard.** Nothing here round-trips the sidecar, and the pages
// stay fully populated while the shard is down. Mounting it under /shard
// would imply a dependency it does not have.
// • **siteMode-gated, like /posts and /wiki.** The shard routes are exempt
// because shard status is wanted *during* maintenance; a bestiary is site
// content and follows site content's rules.
//
// Every route also carries `requireFeature('atlas')` — 404 when an admin has
// disabled the feature, 403 when the caller sits below its configured audience.
// The default audience is `anonymous`, so these gates are inert until an admin
// changes something.
// express and express-validator come from core, never from a require here: this
// file lives outside server/, so Node's resolver would not find them, and a
// second express in the process would be a second Router prototype
// (docs/website/MODULE_API.md §2.3).
const core = require('../core')
const atlas = require('./atlas.controller')
const { requireFeature } = require('../utils/visibility')
const { express, validator, middleware } = core
const { param, query } = validator
const { siteMode, validate } = middleware
const atlasRouter = express.Router()
// Facet names come from the shard's own files and are never validated against a
// list — nothing in the codebase names a facet (§6.1 R2). Only the length is
// bounded, and the query matches exactly, so an unknown name returns an empty
// result rather than an error.
const facetParam = query('facet').optional({ values: 'falsy' }).isString().isLength({ max: 40 })
atlasRouter.get(
'/creatures',
requireFeature('atlas'),
// #swagger.tags = ['Public · Atlas']
// #swagger.summary = 'Search the bestiary (paginated)'
// #swagger.description = 'Every creature the shard spawns, most numerous first. `total` is how many can be alive at once across all spawners; `points` is how many spawners mention it; `facets` maps facet name to that creature\'s share on it. Static content parsed from the shard\'s ServUO tree — unaffected by the shard being offline.'
// #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the creature name (max 60 chars).' }
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to creatures spawning on this facet. Facet names come from the shard\'s own files; an unknown one returns an empty page.' }
// #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Page size, 1..100 (default 50).' }
// #swagger.parameters['offset'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Rows to skip (default 0).' }
/* #swagger.responses[200] = { description: 'A page of creatures plus the unpaginated total', content: { "application/json": { schema: { $ref: "#/components/schemas/AtlasCreaturePage" } } } } */
/* #swagger.responses[403] = { description: 'The atlas feature is gated above this caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[404] = { description: 'The atlas feature is disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }),
facetParam,
query('limit').optional().isInt({ min: 1, max: 100 }),
query('offset').optional().isInt({ min: 0, max: 100000 }),
validate,
siteMode,
atlas.getCreatures,
)
atlasRouter.get(
'/creatures/:slug',
requireFeature('atlas'),
// #swagger.tags = ['Public · Atlas']
// #swagger.summary = 'One creature: where it spawns, and what spawns with it'
// #swagger.description = 'The answer the atlas exists to give. `places` is the aggregate — "lizardman → Shrines, Isamu-Jima, Yew" — resolved by point-in-rect against the shard\'s own region rectangles, falling back to the nearest landmark, else "Wilderness". `spawners` lists the individual spawn points (bounded; `spawnersTruncated` says when the list was cut), and `alsoHere` is what shares those spawners.'
// #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Creature slug, e.g. lizardman.' }
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Restrict places and spawners to one facet.' }
// #swagger.parameters['points'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max spawners to return, 1..1000 (default 200).' }
/* #swagger.responses[200] = { description: 'The creature', content: { "application/json": { schema: { $ref: "#/components/schemas/AtlasCreature" } } } } */
/* #swagger.responses[404] = { description: 'No such creature in this atlas (or the feature is disabled)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('slug').isString().isLength({ min: 1, max: 120 }),
facetParam,
query('points').optional().isInt({ min: 1, max: 1000 }),
validate,
siteMode,
atlas.getCreature,
)
atlasRouter.get(
'/regions',
requireFeature('atlas'),
// #swagger.tags = ['Public · Atlas']
// #swagger.summary = 'Named regions and their rectangles'
// #swagger.description = 'Flattened out of the shard\'s nested Regions.xml. `priority` and the rectangles are what placed each spawn point, kept so the placement can be re-derived rather than taken on trust.'
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet.' }
// #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the region name.' }
/* #swagger.responses[200] = { description: 'Regions, by facet then name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AtlasRegion" } } } } } */
facetParam,
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }),
validate,
siteMode,
atlas.getRegions,
)
atlasRouter.get(
'/landmarks',
requireFeature('atlas'),
// #swagger.tags = ['Public · Atlas']
// #swagger.summary = 'Points of interest (dungeon levels, town markers)'
// #swagger.description = 'From the shard\'s Data/Locations files. `group` is the innermost enclosing parent ("Covetous"), which is the label worth showing over the individual marker ("Level 1").'
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet.' }
// #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the landmark name or its group.' }
/* #swagger.responses[200] = { description: 'Landmarks, by facet then group', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AtlasLandmark" } } } } } */
facetParam,
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }),
validate,
siteMode,
atlas.getLandmarks,
)
atlasRouter.get(
'/champions',
requireFeature('atlas'),
// #swagger.tags = ['Public · Atlas']
// #swagger.summary = 'Configured champion altars (the roster, not the live board)'
// #swagger.description = 'Where the altars are and what each one summons — "there is an Unholy Terror altar in Deceit". `randomType` marks altars whose champion is drawn at activation. Do not conflate this with GET /public/shard/champs, which is the live sidecar-fed board ("it is on level 3 right now").'
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet.' }
/* #swagger.responses[200] = { description: 'Altars, by facet then name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AtlasChampion" } } } } } */
facetParam,
validate,
siteMode,
atlas.getChampions,
)
atlasRouter.get(
'/meta',
requireFeature('atlas'),
// #swagger.tags = ['Public · Atlas']
// #swagger.summary = 'What atlas is loaded: facets, counts, when it was imported'
// #swagger.description = 'Drives the facet filter and the "parsed from the shard\'s own files on <date>" line. Reports the game world only — the ServUO path, the per-file hashes and any pending refresh are operator detail and live on the admin status route.'
/* #swagger.responses[200] = { description: 'Atlas metadata', content: { "application/json": { schema: { $ref: "#/components/schemas/AtlasMeta" } } } } */
siteMode,
atlas.getMeta,
)
module.exports = atlasRouter