Files
runicgateway.com/src/data/capabilities.mjs
wtclaude 556dee7355
All checks were successful
PR checks / checks (pull_request) Successful in 49s
feat(home): phase 3 — the homepage
Replaces phase 1's scaffold with the real homepage: hero, the data path as
inline SVG, the self-hosted argument, all five capability groups, and the
get-started CTA. Three decisions the org lead took first are recorded in
PLAN.md as D17-D19.

The data path is drawn generically and captioned specifically (D17): the nodes
say "your game server" and "sidecar", the sub-labels and caption name ServUO and
uo-link. The SVG is aria-hidden because the four numbered steps beside it carry
the same path in prose — one telling, not two.

The capability list is data with a check behind it (D18). Every Game-intelligence
item names the module-uo capability slug it comes from, and the build fails if
the page and platform.json disagree either way. That needed a fifteenth fact in
checkFacts.mjs: §12 named the capability list as an externally-sourced fact and
nothing re-read it, so the chain rested on someone remembering. It also found
that the site was omitting two of the module's eight capabilities — guilds and
city governors are now listed, in the page and in §10.

The hero leads with the emblem (D19), derived from whichever logo.png is in
force so one file still changes the hero, header, tab icon and app icon
together.

Also here, both found by standing the build up rather than by review:

  - checkBrand.mjs now enforces the demo slot's markup contract. applyBrand.mjs
    reveals the demo link by replacing an exact pair of empty attributes; an
    attribute inserted between them produces a build where the mount sets a demo
    URL, the boot log says nothing and the link never appears. Both halves are
    checked and the literal is derived from the expression applyBrand.mjs uses,
    so they cannot drift.

  - The header nav overflowed at 390px — four links plus the lockup measured
    433px against a 390px viewport, so every phone got a horizontally scrolling
    page. Phase 1 left this to phase 3 expecting a disclosure control; it got a
    wrap instead, because with four links there is nothing to disclose and a
    hamburger costs state, script and duplicate markup.

Verified on a clean checkout of this commit: all four checks, astro check, a
production build, a live /brand/* smoke, and a demo URL mounted and reverted.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 00:03:04 -05:00

173 lines
6.5 KiB
JavaScript

/**
* capabilities.mjs — the five capability groups of PLAN.md §10, as data.
*
* ---------------------------------------------------------------------------------------
* WHY THIS IS DATA AND NOT MARKUP
* ---------------------------------------------------------------------------------------
* The homepage names these groups, `/features/` (phase 4) expands them, and `/modules/`
* explains the core/module split they encode. Three pages listing the same capabilities in
* three hand-maintained lists is how a site ends up advertising something that was removed,
* which §1 forbids. One list, read by all three.
*
* ---------------------------------------------------------------------------------------
* THE PART THAT IS A CHECK, NOT A LIST
* ---------------------------------------------------------------------------------------
* "Game intelligence" is the only group core does not supply — it comes from whichever
* module is installed, and today that is `module-uo`. Its items therefore carry the
* capability slugs the module actually declares in its `module.json`, and
* `assertCapabilityCoverage()` fails the build if the two lists drift apart.
*
* That closes a real gap. `platform.json` holds `moduleUoCapabilities` and
* `scripts/checkFacts.mjs` re-reads it from the module's manifest on every build — so the
* day `module-uo` gains a capability, the JSON goes red and someone updates it. Before this
* function, updating the JSON was the end of it and the page kept the old list. Now the
* page is what goes red next.
*
* Note that slugs are NOT one-per-item in either direction: `shard` is the source of four
* separate user-facing capabilities, and the marketplace draws on `market` and `cliloc`
* together (item names arrive as cliloc ids and are resolved against the shard's own
* string table). The check is coverage in both directions, not a bijection.
*/
/**
* Community — core, game-agnostic. Everything here works on a deployment with no game
* module installed at all.
*/
const community = {
id: 'community',
/**
* Core, not module-supplied. Stated on every group rather than only on the one that is
* true, so the shape of a group is uniform — the homepage reads this field on all five,
* and an inferred union that carries it on one member is an error waiting for the next
* template that touches it.
*/
moduleSupplied: false,
title: 'Community',
summary: 'The site your players actually use, none of which knows what game you run.',
items: [
{ label: 'Teams' },
{ label: 'Team forums' },
{ label: 'Notifications' },
{ label: 'Wiki' },
{ label: 'News and newsletter' },
{ label: 'Player self-service' },
],
};
/**
* Game intelligence — module-supplied. The `caps` arrays are the contract with
* `platform.json`; see `assertCapabilityCoverage` below.
*/
const gameIntelligence = {
id: 'game-intelligence',
title: 'Game intelligence',
moduleSupplied: true,
summary:
'Supplied by the installed game module, not by the core site. Today that module is ' +
'module-uo, and this is what it publishes from a live shard.',
items: [
{ label: 'Live server status', caps: ['shard'] },
{ label: 'Economy and activity', caps: ['shard'] },
{ label: 'Character sheets', caps: ['shard'] },
{ label: 'Points and loyalty boards', caps: ['shard'] },
{ label: 'Player-vendor marketplace', caps: ['market', 'cliloc'] },
{ label: 'Houses and IDOC decay', caps: ['houses'] },
{ label: 'Spawn atlas', caps: ['atlas'] },
{ label: 'Champion boards', caps: ['champs'] },
{ label: 'Guilds', caps: ['guilds'] },
{ label: 'City governors', caps: ['governors'] },
],
};
const administration = {
id: 'administration',
moduleSupplied: false,
title: 'Administration',
summary: 'Running the place, with a record of who did what.',
items: [
{ label: 'Roles and permissions' },
{ label: 'Moderation and appeals' },
{ label: 'Content reports' },
{ label: 'Append-only audit log' },
{ label: 'Bot scoring and IP bans' },
{ label: 'Module management' },
{ label: 'The game-server connection' },
],
};
const integration = {
id: 'integration',
moduleSupplied: false,
title: 'Integration',
summary: 'The seams that let other things reach in — and one game reach out.',
items: [
{ label: 'Modules' },
{ label: 'The sidecar bridge' },
{ label: 'Discord: slash commands, notifications, voice' },
{ label: 'Mobile and push' },
{ label: 'SSO over OAuth2 / OIDC' },
],
};
const infrastructure = {
id: 'infrastructure',
moduleSupplied: false,
title: 'Infrastructure',
summary: 'How it runs, and who it answers to.',
items: [
{ label: 'Self-hosted, start to finish' },
{ label: 'Docker, with prebuilt pull-only images' },
{ label: 'Branding as data, not a rebuild' },
{ label: 'OpenAPI 3.0 for the whole API' },
],
};
export const capabilityGroups = [
community,
gameIntelligence,
administration,
integration,
infrastructure,
];
/**
* Fails the build when the module's declared capabilities and this page's list disagree.
*
* Called from the component rather than from a check script on purpose: the failure needs
* to reach whoever is editing the page, and an Astro build error names the component. It
* also means the rule cannot be skipped by running `astro build` without `npm run verify`.
*/
export function assertCapabilityCoverage(declared) {
const claimed = new Set();
for (const item of gameIntelligence.items) {
for (const cap of item.caps || []) claimed.add(cap);
}
const known = new Set(declared);
const unlisted = declared.filter((cap) => !claimed.has(cap));
const invented = [...claimed].filter((cap) => !known.has(cap));
if (!unlisted.length && !invented.length) return;
const lines = [];
if (unlisted.length) {
lines.push(
`the installed module declares ${unlisted.map((c) => `"${c}"`).join(', ')}, which no ` +
`capability on the homepage claims — the site is under-selling what it can show.`
);
}
if (invented.length) {
lines.push(
`the homepage claims ${invented.map((c) => `"${c}"`).join(', ')}, which the module no ` +
`longer declares — the site is advertising something that is gone (§1).`
);
}
throw new Error(
`src/data/capabilities.mjs disagrees with platform.json's moduleUoCapabilities:\n` +
lines.map((line) => ` - ${line}`).join('\n') +
`\n\nUpdate the "Game intelligence" items, or the JSON if the module itself changed.\n`
);
}