docs(teams): the Teams bet, as built (Teams cutover 6/6) #169
@@ -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:
|
||||
<Slot name="uo.guild.header" externalId={guildId} moduleId="uo" />
|
||||
@@ -1304,6 +1313,33 @@ registry.declareModuleSlot(ID, 'uo.guild.forum')
|
||||
<Slot name="uo.guild.forum" externalId={guildId} moduleId="uo" />
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user