docs(teams): Teams is a contract, not a surface — record the correction
The org lead's correction to Part 3, and the inverted extension-slot direction it forces. TEAMS.md: §3.1's routes, §3.4's two slots and §3.5's three nav entries are all marked superseded in place, and Part 12's phase 3 entry gains the amendment explaining why — core does not own the word for a Team, so the module that owns the vocabulary owns the page. The five corrections found by building are kept alongside it. MODULE_API.md: 1.6.0's list swaps the two client slots for registry.declareModuleSlot + Slot in the UI kit, and a new §3.7a documents the inverted direction: what forced it, the enforced namespace, why core's fills are applied at mount rather than eagerly, and why a fill for an undeclared slot is a no-op where §3.7's unknown slot throws. BACKEND_DESIGN.md: the by-external-id lookup route. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -36,14 +36,21 @@ module chunk evaluates, which is earlier than any network round trip could answe
|
||||
**1.6.0 — Teams, the whole surface.** Eight additions, no removals and no changed signature, so minor;
|
||||
`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` · `api.registerSlashCommands(...)` · the client slots
|
||||
`team.overview` and `team.member.row`.
|
||||
the provider's optional `projectRoster` · `api.registerSlashCommands(...)` ·
|
||||
`registry.declareModuleSlot(...)` with `Slot` in the UI kit.
|
||||
|
||||
> **Amended 2026-08-17 (phase 3), on the org lead's decision: the eighth member joins 1.6.0 in place
|
||||
> rather than getting a 1.7.0.** The rule is the one Protocol 4 was given in phase 2 — *a contract
|
||||
> owes a bump only once it has landed on `main`* — and 1.6.0 has only ever been on `edge`. The same
|
||||
> amendment marks `ctx.teams.activity.push` and both client slots live, and narrows
|
||||
> `team.member.row`'s props (TEAMS.md §3.4).
|
||||
> **Amended 2026-08-17 (phase 3), on the org lead's decision.** Two changes.
|
||||
>
|
||||
> **The eighth member joins 1.6.0 in place rather than getting a 1.7.0.** The rule is the one Protocol
|
||||
> 4 was given in phase 2 — *a contract owes a bump only once it has landed on `main`* — and 1.6.0 has
|
||||
> only ever been on `edge`. `ctx.teams.activity.push` is live now rather than throwing.
|
||||
>
|
||||
> **The client slots `team.overview` and `team.member.row` are replaced by the INVERTED direction.**
|
||||
> 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.
|
||||
|
||||
**The number covers the whole surface; the members arrive by phase, and each is marked below.** Seven
|
||||
are live now. `api.registerSlashCommands` is **present and throws**, with an error naming the phase
|
||||
@@ -942,6 +949,7 @@ The kit is **curated and closed**, not a re-export of `components/`:
|
||||
| `Loading`, `ErrorState`, `EmptyState` | `components/PageState.jsx` | the three states every data page has |
|
||||
| `useAsync` | `lib/useAsync.js` | the fetch/loading/error hook every data page uses |
|
||||
| `useAuth`, `useSite` | `contexts/*` | read-only access to session and site settings |
|
||||
| `Slot` *(1.6.0)* | `modules/Slot.jsx` | renders a place this module declared for core to fill (§3.7a) |
|
||||
|
||||
Everything else — tables, chips, tabs, the tiptap editor, dnd-kit — a module bundles itself.
|
||||
Adding to the kit is a **minor** `MODULE_API_VERSION` bump; *changing* a kit component's existing
|
||||
@@ -1178,6 +1186,53 @@ intends to fill permanently, and a slot core *does* intend to fill is a slot tha
|
||||
members of the `registry` object handed to modules, for the same reason `featureProviders()` is not:
|
||||
declaring is core's, and so is reading back who filled what.
|
||||
|
||||
### 3.7a Inverted slots — CORE content inside a MODULE's page *(1.6.0)*
|
||||
|
||||
The mirror of §3.7, added for Teams. §3.7 assumes core owns the page and a module contributes to it,
|
||||
which is right for the footer and the admin user detail. This is the other shape, and the case that
|
||||
forced it is worth stating because it will recur:
|
||||
|
||||
> **A core primitive whose vocabulary core does not own.** Teams are core's — core owns the tables,
|
||||
> the reconciler, the access resolver and the activity feed — but core has no word for one. A UO shard
|
||||
> calls them guilds; the next game will call them clans. A core-rendered `/teams` page would publish a
|
||||
> noun core invented, beside the module's own page for the same thing. So the **page** is the
|
||||
> module's, and the parts core cannot hand over — here the activity feed, whose public/members split
|
||||
> only core can resolve — are contributed to it.
|
||||
|
||||
```js
|
||||
// In the module's entry chunk, at registration time:
|
||||
registry.declareModuleSlot(ID, 'uo.guild.detail')
|
||||
|
||||
// In the module's page, from the UI kit:
|
||||
<Slot name="uo.guild.detail" externalId={guildId} moduleId="uo" />
|
||||
```
|
||||
|
||||
**The name must be namespaced under the declaring module's id**, and that is enforced rather than
|
||||
conventional: it is the only thing keeping two modules from claiming one name, and it makes the owner
|
||||
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
|
||||
`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
|
||||
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
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Part 4 — The loader's obligations
|
||||
|
||||
Reference in New Issue
Block a user