Files
runicgateway.com/src/components/architecture/ModuleSeam.astro
wtclaude 2d19ee4220
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
feat(marketing): phase 4 — the marketing pages
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and
/community/ — plus the two scope items the phase table never assigned to anyone.

Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as
D20-D25:

- D20 /features/ is the homepage's list with a `detail` line, not a second list.
  One data file, two renderings, so they cannot disagree about what exists.
- D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per
  boundary. No endpoint tables, no config keys — those are phase 8's and stay
  canonical in docs/.
- D22 The deliberate absences of §2 become one tagged data file, rendered on the
  three pages that promise them.
- D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the
  header since phase 1, built by no phase) and checkLinks.mjs.
- D24 `needsModule`: writing the Teams detail exposed a false claim phase 3
  shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is
  no create route, sync is gated on providerModuleId() — so the Community group
  no longer says a bare core does all of it.
- D25 The per-capability demo affordance brand.json had promised since phase 2 is
  a deep link, filled at boot from data-demo-path.

checkLinks.mjs reads the built HTML rather than src/, because half these links
are assembled from data files and template literals. Its PLANNED_ROUTES list is
checked in both directions, so it cannot rot into a permanent exemption.

applyBrand.mjs gained a pass that recomputes deep links from their immutable
path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out
and runs it against the stock markup so the two cannot drift. Both proved
against a real mount, in both directions.

Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped
component class are both specificity 0,1,0, so .demo-link's `display` beat the
hide rule and twelve links to a nonexistent demo rendered, each resolving to the
current page. The rule is now !important.

The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css.

Verified from a clean checkout: npm ci, all five checks, astro check (0 errors),
production build, and a live browser pass at desktop and 390px.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 01:53:11 -05:00

167 lines
7.5 KiB
Plaintext

---
import platform from '../../data/platform.json';
/**
* "Where the game stops and the platform starts" — the third of `/architecture/`'s diagrams
* (D21).
*
* The other two draw runtime shapes. This one draws a code boundary, and it is here because
* it is the claim the whole project rests on: that a community platform can be built once
* and pointed at any game. An evaluator has every reason to read that as marketing, so the
* page draws the seam and then says plainly what does and does not prove it — one module
* exists, the second is a paper exercise, and the exit criterion for calling the contract
* proven is written down (§2, and the entries `/modules/` renders from `notBuilt.mjs`).
*
* The Module API version is read from `platform.json` like every other number on this site
* (§12). It is the one place a version genuinely belongs in this diagram: the seam is
* literally a version check, and a module whose declared range does not match refuses to
* load rather than half-loading.
*/
---
<section class="page section diagram" id="module-seam">
<div class="diagram__head">
<p class="eyebrow">Where the game stops</p>
<h2>A seam, with a version on it</h2>
<p class="prose">
The core site does not know what a shard is, what a guild is, or that Ultima Online
exists. Everything that does lives in an installable module on the other side of a
declared interface — which is what makes "put your game on it" a shape rather than a
slogan.
</p>
</div>
<div class="diagram__body">
<div class="diagram__figure">
<svg viewBox="0 0 380 500" class="flow" aria-hidden="true" focusable="false">
<!-- Core: what ships in the image, on every deployment, module or not. -->
<rect class="host" x="8" y="8" width="364" height="186" rx="14" />
<text class="host-title" x="28" y="42">Runic Gateway core</text>
<text class="host-sub" x="28" y="62">game-agnostic; the same image everywhere</text>
<rect class="node node--self" x="28" y="80" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="108">Accounts</text>
<rect class="node node--self" x="196" y="80" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="108">Teams</text>
<rect class="node node--self" x="28" y="134" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="162">Wiki and posts</text>
<rect class="node node--self" x="196" y="134" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="162">Admin and API</text>
<!-- The seam. Both boundary lines and the label between them: this is the one
thing in the picture that is neither core nor module. -->
<path class="boundary" d="M8 224 H372" />
<text class="seam-label" x="190" y="252" text-anchor="middle">
Module API {platform.moduleApi}
</text>
<path class="boundary" d="M8 272 H372" />
<!-- Registers upward; is asked downward. Two arrows, opposite directions, because
the traffic across a seam is not one-way and drawing it as one-way is what
makes people think a module is a plugin that only listens. -->
<path class="spine" d="M120 300 V206" />
<path class="arrow" d="M120 198 l-6 10 h12 Z" />
<text class="seam-arrow" x="136" y="216">registers</text>
<path class="spine" d="M260 200 V294" />
<path class="arrow" d="M260 302 l-6 -10 h12 Z" />
<text class="seam-arrow" x="244" y="290" text-anchor="end">calls</text>
<!-- The module: everything that knows a game exists. -->
<rect class="host" x="8" y="306" width="364" height="186" rx="14" />
<text class="host-title" x="28" y="340">Game module</text>
<text class="host-sub" x="28" y="360">one per deployment; UO today</text>
<rect class="node" x="28" y="378" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="406">Routes</text>
<rect class="node" x="196" y="378" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="406">Screens</text>
<rect class="node" x="28" y="432" width="156" height="46" rx="10" />
<text class="node-title" x="46" y="460">Its own tables</text>
<rect class="node" x="196" y="432" width="156" height="46" rx="10" />
<text class="node-title" x="214" y="460">Nav rows</text>
</svg>
<p class="diagram__caption">
A module declares which versions of the interface it speaks. If that does not match
what the site offers, it refuses to load and the site comes up without it.
</p>
</div>
<div class="diagram__notes">
<section>
<h3>The module brings its own everything</h3>
<p>
Not just screens: its routes, its database tables, its navigation rows, its slice of
the OpenAPI spec and its own prebuilt client bundle. Installing it is a paste in the
admin panel or a line in your environment — never a build step, because production
runs an image you pulled, and an operator who has to compile something has been
handed a maintenance job rather than a feature.
</p>
</section>
<section>
<h3>Failure is contained by design</h3>
<p>
A module that will not load is marked as failed and the site starts without it.
Disabling one is a kill switch, not a visibility flag — its routes stop answering
and its live connections close. Uninstalling keeps the data, and destroying the data
is a separate, deliberate choice made in its own dialog.
</p>
</section>
<section>
<h3>Teams is the shape of the contract</h3>
<p>
Core owns the Teams primitive — the roster, the forum, the notifications, the voice
channel — and does not own the <em>word</em>. A Team cannot be created in core at
all; it arrives from the module, which is why the UO module calls them guilds and
builds those pages itself. That is the pattern the whole interface is built on: core
supplies the machinery, the module supplies the meaning.
</p>
</section>
<section>
<h3>What this does not yet prove</h3>
<p>
One module exists and it is Ultima Online. A second, for a different game, is a
written dry-run that was deliberately never implemented — it exists to test whether
the contract generalises on paper. Until somebody builds the second one, the seam is
a well-argued design rather than a demonstrated one, and this site says so wherever
it comes up.
</p>
</section>
</div>
</div>
</section>
<style>
/* The seam label sits between the two boundary rules rather than beside them: it is
the name of the gap, not an annotation on either side of it. Gold, because it is
the one contract in the picture. */
.seam-label {
fill: var(--gold);
font-family: var(--sans);
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* Two words, because two arrows crossing a boundary in opposite directions is
ambiguous without them — and the ambiguity is the exact misreading this diagram
exists to prevent, that a module is something core talks at. */
.seam-arrow {
fill: var(--dim);
font-family: var(--sans);
font-size: 11px;
font-style: italic;
}
</style>