From d10f5809b3bc2c7ab60a70930f6e1a3c75fafbdd Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 19 Aug 2026 01:16:07 -0500 Subject: [PATCH] docs(modules): core offers a contribution, never a slot name Amends MODULE_API 1.6.0 in place - it has only ever been on edge, the same rule the eighth and ninth members were given - and it is a correction rather than an addition. As first written, the inverted slot direction had core fill three literal uo.guild.* names. That worked for module-uo and silently did nothing for anyone else: a module declaring clan.detail under its own id got an empty page and no error, because "a fill for a slot nobody declared is not an error" is exactly the rule that makes an unknown name invisible. It also put a module identifier inside core, in string literals the Sec 5.2 checker masks by construction. Sec 3.7a now documents declareModuleSlot(id, name, { core }) and the three contributions core offers - team.activity, team.forum, team.notify - as a table, with the rules that follow from the direction: the member is optional, a slot that asks for nothing stays empty, more than one slot may ask for the same contribution, and asking for one core does not offer THROWS at the declaration rather than rendering empty forever. TEAMS.md's two accounts of the inversion (Part 3's supersession note and the phase 3 amendment) say the same thing. Also corrects the UI kit's count in Sec 3.4 and Sec 3.7a: Slot made it nine in phase 3 and three places still said eight. Found by phase 11 while writing the chapter that teaches this shape to an audience outside this org. Co-Authored-By: Claude --- website/MODULE_API.md | 66 +++++++++++++++++++++++++++++++++---------- website/TEAMS.md | 35 +++++++++++++---------- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/website/MODULE_API.md b/website/MODULE_API.md index ab34b09..74c0611 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -37,8 +37,16 @@ module chunk evaluates, which is earlier than any network round trip could answe `module-uo`'s `coreApi: "^1.3.0"` still resolves. `api.registerTeamProvider(...)` and `ctx.teams.publish` / `ctx.teams.reconcile` (§2.3, §2.4a) · `ctx.teams.activity.push` · the provider's optional `projectRoster` and `pageUrlTemplate` · `api.registerSlashCommands(...)` · -`registry.declareModuleSlot(...)` with `Slot` in the UI kit. +`registry.declareModuleSlot(...)` with `Slot` in the UI kit — the ninth member of it. +> **Amended 2026-08-19 (phase 11), on the org lead's decision.** `declareModuleSlot` takes an +> optional `{ core }` naming which of core's contributions belongs in the declared place, and core +> offers contributions instead of naming slots (`CORE_CONTRIBUTIONS`, §3.7a). In 1.6.0 in place, by +> the same rule as the two amendments below: 1.6.0 has only ever been on `edge`. It is a **correction +> and not an addition** — as first written, core filled three literal `uo.guild.*` names, so the +> inverted direction worked for exactly one module and silently did nothing for any other, which the +> integration kit found while trying to teach it to an audience outside this org. +> > **Amended 2026-08-17 (phase 3), on the org lead's decision.** Two changes. > > **Amended again 2026-08-18 (phase 6), on the org lead's decision.** A **ninth** member, @@ -56,8 +64,8 @@ the provider's optional `projectRoster` and `pageUrlTemplate` · `api.registerSl > Both assumed core rendered a Team page. It does not: **Teams is a contract primitive, not a > surface** — core owns the tables, the sync, the access rules and the activity feed, and does not own > the word for one, so the module that owns the vocabulary owns the page. In their place, -> `registry.declareModuleSlot(id, name)` lets a MODULE declare a place on its own page for CORE to -> fill, and `Slot` joins the UI kit so the module can render it. See §3.7a. +> `registry.declareModuleSlot(id, name, { core })` lets a MODULE declare a place on its own page for +> CORE to fill, and `Slot` joins the UI kit so the module can render it. See §3.7a. **Every member of 1.6.0 is live as of phase 7.** `api.registerSlashCommands` was the last one still throwing, and it now registers — the staged rollout the paragraphs above describe is finished. A @@ -1050,7 +1058,7 @@ props is a **major** one, because that breaks a call already written. Adding an minor (§1.1). That is a real constraint on core and it is the price of the boundary being worth anything. -The kit is those **eight exports** — five rows, because `PageState` contributes three. An earlier +The kit is those **nine exports** — six rows, because `PageState` contributes three. An earlier draft of this table listed a ninth, `AdminPage`, and core has no such component — admin views are plain markup inside `AdminLayout`. It was struck in Phase 2 PR 7 rather than satisfied by inventing a core component with no consumer until Phase 3; adding it later costs a minor bump, which is the case @@ -1293,10 +1301,11 @@ forced it is worth stating because it will recur: > only core can resolve — are contributed to it. ```js -// In the module's entry chunk, at registration time: -registry.declareModuleSlot(ID, 'uo.guild.header') -registry.declareModuleSlot(ID, 'uo.guild.detail') -registry.declareModuleSlot(ID, 'uo.guild.forum') +// In the module's entry chunk, at registration time. The second argument names +// which of CORE's contributions belongs in that place: +registry.declareModuleSlot(ID, 'uo.guild.header', { core: 'team.notify' }) +registry.declareModuleSlot(ID, 'uo.guild.detail', { core: 'team.activity' }) +registry.declareModuleSlot(ID, 'uo.guild.forum', { core: 'team.forum' }) // In the module's page, from the UI kit: @@ -1304,6 +1313,33 @@ registry.declareModuleSlot(ID, 'uo.guild.forum') ``` +**Core offers a CONTRIBUTION; it never names a slot.** This is the part a second game depends on, and +the first cut of 1.6.0 had it the other way round — core filled the three literal names above, which +worked for `module-uo` and silently did nothing for anybody else: a module declaring `clan.detail` +under its own id got an empty page and no error, because "a fill for a slot nobody declared is not an +error" is exactly the rule that makes an unknown name invisible. It also put a module identifier +inside core, in three string literals `scripts/checkModuleIdentifiers.js` masks by construction and +could never have caught (§5.2). Corrected inside 1.6.0, before it reached `main`. + +So the module says WHERE, in its own vocabulary, and WHICH of core's contributions goes there: + +| Contribution *(1.6.0)* | What core puts in the slot | Why it is core's | +| --- | --- | --- | +| `team.activity` | the Team activity feed | only core can resolve the public/members split on it | +| `team.forum` | the Team forum panel | membership and manual grants are core's rules | +| `team.notify` | the per-Team notification control | core resolves whether the viewer is in the Team | + +`options.core` is **optional** — a module may declare a place it fills itself, or one it is keeping +empty for now. Asking for a contribution core does not offer **throws at the declaration**, and that +asymmetry with an unfilled slot is deliberate: core's catalogue is fixed at build time and the +module's `coreApi` range has already been checked, so an unknown contribution is always a typo or a +version skew, and the alternative failure is a page that renders empty forever with nothing logged. +**Adding a contribution is a minor bump**; removing one is major. + +More than one slot may ask for the same contribution and each gets it. Core has no reason to care how +many places a module wants its feed in, and refusing the second would be core making a layout decision +on a page it does not own. + **A module declares one slot per PLACE, not one per page.** `module-uo` declares **three** on the same guild page — core fills them with the Team notification control, the activity feed and the Team forum — because a slot holds one component and the first fill wins. Collapsing them would hand core the @@ -1318,25 +1354,25 @@ readable at the fill site. **Core fills these at MOUNT, not eagerly, and the ordering is why the call exists at all.** Core's bundle evaluates before every module chunk (§3.1), so at the moment core would like to fill one of -these the slot does not exist. Core registers its intent (`fillModuleSlot`, core-only) and +these the slot does not exist. Core registers its intent (`offerCoreFill`, core-only) and `applyCoreFills()` runs once, from `main.jsx`, after every chunk has evaluated and before the first render. -**A fill for a slot no installed module declares is a no-op, never an error.** The declaring module is -simply not installed, which is the ordinary case on any deployment — the exact mirror of an unfilled -slot rendering nothing. Note the asymmetry with §3.7, where an unknown slot throws: there, an unknown +**A contribution nothing asks for is a no-op, never an error.** No game module is installed, which is +the ordinary case on any deployment — the exact mirror of an unfilled slot rendering nothing. Note the asymmetry with §3.7, where an unknown slot throws: there, an unknown name is always a typo or a version skew, because core declares before any module can name one. **First fill still wins**, so a module that fills its own declared slot keeps it and core's fill is skipped. That is deliberate: the module owns the page. -**`Slot` is the eighth member of the UI kit** (§3.4) for this. A module could not render one of these +**`Slot` is the ninth member of the UI kit** (§3.4) for this. A module could not render one of these otherwise, and reimplementing it would mean a second error boundary with different behaviour — which matters more here than anywhere else in the kit, because the thing being contained is *core's* content failing inside the *module's* page. -`declareModuleSlot` is on the `registry` object handed to modules. `fillModuleSlot` and -`applyCoreFills` are not: filling one of these is core's, exactly as declaring a §3.7 slot is. +`declareModuleSlot` is on the `registry` object handed to modules. `offerCoreFill`, `applyCoreFills` +and `CORE_CONTRIBUTIONS` are not: offering into one of these is core's, exactly as declaring a §3.7 +slot is. --- diff --git a/website/TEAMS.md b/website/TEAMS.md index 650ab2c..849698d 100644 --- a/website/TEAMS.md +++ b/website/TEAMS.md @@ -830,18 +830,20 @@ named for a *place* and never for a meaning): > **Superseded 2026-08-17 (phase 3, org lead).** Both slots are gone, and the DIRECTION is what > changed. They assumed core rendered the Team page; core renders no Team page. The replacement is -> `registry.declareModuleSlot(id, name)` — a **module** declares a place on its own page, namespaced -> under its own id, and **core** fills it: +> `registry.declareModuleSlot(id, name, { core })` — a **module** declares a place on its own page, +> namespaced under its own id, naming which of core's contributions goes there, and **core** offers it: > > | Slot | Declared by | Rendered in | Filled by core with | Props | > | --- | --- | --- | --- | --- | -> | `uo.guild.detail` | `module-uo` | its guild detail page | the Team activity feed (§4.3) | `{ externalId, moduleId }` | -> | `uo.guild.forum` | `module-uo` | the same page, below the feed | the Team forum (Part 5) — added in phase 4 | `{ externalId, moduleId }` | +> | `uo.guild.detail` | `module-uo` | its guild detail page | `team.activity` — the Team activity feed (§4.3) | `{ externalId, moduleId }` | +> | `uo.guild.forum` | `module-uo` | the same page, below the feed | `team.forum` — the Team forum (Part 5), added in phase 4 | `{ externalId, moduleId }` | > -> Core's fills are applied at MOUNT, not eagerly: core's bundle evaluates before every module chunk, -> so when core registers a fill the slot does not exist yet. A fill for a slot no installed module -> declares is a no-op, not an error — the mirror of an unfilled slot rendering nothing. `Slot` becomes -> the eighth member of the shared UI kit so a module renders the place with core's own error boundary, +> Core's contributions are applied at MOUNT, not eagerly: core's bundle evaluates before every module +> chunk, so when core offers one, no module-declared slot exists yet. A contribution nothing asks for +> is a no-op, not an error — the mirror of an unfilled slot rendering nothing. **Core names the +> contribution and never the slot** (amended phase 11, inside 1.6.0: as first built it filled the +> literal names above, which reached `module-uo` and no other game). `Slot` becomes +> the ninth member of the shared UI kit so a module renders the place with core's own error boundary, > which matters here because the thing being contained is CORE's content failing inside the MODULE's > page. > @@ -2535,13 +2537,16 @@ guild called "Admin" cannot put an official-looking page on the site. > > **So the extension slots invert, and that is a new `MODULE_API` §3.7 direction.** `team.overview` > and `team.member.row` assumed core rendered the page. They are replaced by -> `registry.declareModuleSlot(id, name)`: a **module** declares a place on its own page, namespaced -> under its own id, and **core** fills it. `module-uo` declares `uo.guild.detail`; core fills it with -> the activity feed, because only core can resolve whether a viewer is inside the Team and the -> public/members split is a security boundary. Core's fills are applied at mount rather than eagerly — -> core's bundle evaluates before every module chunk, so at the moment core registers a fill the slot -> does not exist yet. `Slot` joins the shared UI kit as its eighth member so the module renders the -> place with core's own error boundary. +> `registry.declareModuleSlot(id, name, { core })`: a **module** declares a place on its own page, +> namespaced under its own id, and names which of core's contributions belongs there. `module-uo` +> declares `uo.guild.detail` and asks for `team.activity`; core offers the activity feed, because only +> core can resolve whether a viewer is inside the Team and the public/members split is a security +> boundary. **Core names the contribution, never the slot** — amended in phase 11, inside 1.6.0, after +> the integration kit found that the literal-name version worked for one module and silently did +> nothing for any other. Core's contributions are applied at mount rather than eagerly — core's bundle +> evaluates before every module chunk, so at the moment core offers one, no module-declared slot exists +> yet. `Slot` joins the shared UI kit as its ninth member so the module renders the place with core's +> own error boundary. > > **A module names a Team in its own vocabulary**, so `GET /public/teams/by-external/:moduleId/:externalId` > is added: core's row id and slug are core-internal and handing them to a module is how a module ends