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 06f5f9b..8d1e265 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. > @@ -2547,13 +2549,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