From 9d0a1970089e4e4afaa860a96f4a97a91e54f977 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 18 Aug 2026 07:24:37 -0500 Subject: [PATCH 1/2] feat(guilds): declare a second place on the guild page, for core's forum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mirror of the activity feed, one phase later. Core owns the Team forum — membership, manual grants and the member/guest split are all core's rules, and a module reimplementing any of them would be reimplementing a security boundary — but core publishes no Team page, because it does not own the word "guild". So this module declares the place and core puts the forum in it. TWO declarations rather than one, and that is the interesting part. A slot holds one component and the first fill wins, so folding the forum into `uo.guild.detail` alongside the feed would hand core the decision about where each of its two contributions sits on a page this module owns. Separate slots also keep them independent: with the forum switched off, the feed renders exactly as before. The registration test now asserts the set of declared slots and that EVERY one of them is rendered by the page that owns it, rather than naming a single slot twice. A slot nothing renders is a slot core fills into the void. Co-Authored-By: Claude --- client/src/entry.jsx | 8 ++++++++ client/src/routes/public/Guild.jsx | 5 +++++ client/test/registration.test.js | 18 ++++++++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/client/src/entry.jsx b/client/src/entry.jsx index 1c10dc5..ef9177d 100644 --- a/client/src/entry.jsx +++ b/client/src/entry.jsx @@ -194,6 +194,14 @@ registry.registerExtension(ID, 'player.invite.accepted', InviteGameAccountStep) // enough; on a core that knows nothing of Teams it simply stays empty. registry.declareModuleSlot(ID, 'uo.guild.detail') +// A SECOND place on the same page, for core's Team forum (TEAMS.md Part 5). Two +// declarations rather than one, because a slot holds one component and this module +// wants to decide where each of core's two contributions sits on its own page — +// the feed reads as part of the guild's story, the forum is a room you go into. +// Neither knows the other exists, and a core that fills only one leaves the other +// empty. +registry.declareModuleSlot(ID, 'uo.guild.forum') + // `module.json`'s `coreApi` range is checked by the loader before this file is // ever served, so there is nothing to re-check here. It is logged because a // mismatch between the core that validated the manifest and the core that diff --git a/client/src/routes/public/Guild.jsx b/client/src/routes/public/Guild.jsx index 91ce62c..945cc21 100644 --- a/client/src/routes/public/Guild.jsx +++ b/client/src/routes/public/Guild.jsx @@ -102,6 +102,11 @@ export default function Guild() { that does not fill it, or when there is nothing to show. The guild is named in OUR terms — core maps its own Team from these two. */} + + {/* And the Team forum, in its own place below the feed. Core resolves + who may read it — membership and manual grants are core's rules — + so this module renders the room and never its door policy. */} + )} diff --git a/client/test/registration.test.js b/client/test/registration.test.js index f3a124c..5378bea 100644 --- a/client/test/registration.test.js +++ b/client/test/registration.test.js @@ -209,16 +209,22 @@ it('registers under exactly one module id, matching the manifest', () => { assert.deepEqual([...owners], [manifest.id]) }) -it('declares its own guild-detail slot, for core to fill', () => { +it('declares its own guild slots, for core to fill', () => { // The inverted direction (TEAMS.md Part 3). Teams are a core primitive with no - // core page: core owns the activity feed and this module owns the word "guild", - // so this module declares the place and core puts the feed in it. - assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail']) + // core page: core owns the activity feed and the forum, this module owns the + // word "guild", so this module declares the places and core puts them in. + // + // TWO slots rather than one because a slot holds one component: stacking the + // feed and the forum into a single fill would take away this module's ability + // to place them separately on its own page. + assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail', 'uo.guild.forum']) }) -it('the declared slot is rendered by the page that owns it', () => { +it('every declared slot is rendered by the page that owns it', () => { // A slot nothing renders is a slot core fills into the void. Asserted against // the source rather than the chunk, since the chunk is minified. const page = fs.readFileSync(path.resolve(HERE, '..', 'src', 'routes', 'public', 'Guild.jsx'), 'utf8') - assert.match(page, /name="uo\.guild\.detail"/) + for (const name of registered.declaredSlots) { + assert.match(page, new RegExp(`name="${name.replace(/\./g, '\.')}"`)) + } }) -- 2.49.1 From 46e3f5a1277fe355526b24d60e3be95299e6a1d9 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 18 Aug 2026 07:31:58 -0500 Subject: [PATCH 2/2] ci(core-ref): bump the pin past registerTeamProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `frozen-manifest` has been failing since Teams phase 2, on this PR and on #11 before it, for a reason that has nothing to do with either: the pinned core (website#140, the module-system de-UO slice) predates `api.registerTeamProvider`, which this module has called since phase 1 of its Teams work. The module therefore fails to LOAD in the pinned checkout — "api.registerTeamProvider is not a function" — and a module that does not load adds no routes, which the job correctly reports as the module having removed everything it serves. So the red was real and was pointing at the pin, exactly as the pin's own comment says it should: core moves for reasons that have nothing to do with this module, and a bump is a deliberate commit saying which core the module was last proved against. Bumped to `edge` at Teams phase 3 (website#152) — the first core that has both `registerTeamProvider` and the roster projection this module now implements. Reproduced the whole job locally against that core: core's own manifest is current at the new pin, the module loads, and the difference is 73 routes, all documented. `routes.manifest.json` is unchanged and needed no regeneration, which is the expected result for a client-only change. Not bumped to the phase 4 core, deliberately: that is website#153 and is not merged yet. Nothing in this module needs it — the second slot is a client-side declaration, invisible to the route manifest. Co-Authored-By: Claude --- ci/core-ref.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/core-ref.json b/ci/core-ref.json index 9e78853..023530a 100644 --- a/ci/core-ref.json +++ b/ci/core-ref.json @@ -1,6 +1,6 @@ { "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/uo and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves. Pinned rather than tracking `edge` on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together.", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", - "ref": "87230c879aa6e9adde3507718aed6bc4e4d86009", - "refName": "edge @ phase 3 slice 4 (website#140)" + "ref": "7ed2ac99838f4bd64e1df324fe4961673648b0e6", + "refName": "edge @ Teams phase 3 (website#152)" } -- 2.49.1