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:
@@ -759,6 +759,11 @@ they land in core's committed manifest), and a matching `BACKEND_DESIGN.md` edit
|
||||
|
||||
### 3.1 Routes and shell
|
||||
|
||||
> **Superseded 2026-08-17 (phase 3, org lead).** The four public/player rows below are NOT core's.
|
||||
> Teams is a contract primitive and core does not own the vocabulary, so the module that owns the word
|
||||
> owns the page: `module-uo` renders these under `/uo/guilds`. Only the two `/admin/teams` rows are
|
||||
> core's. See the phase 3 amendment in Part 12.
|
||||
|
||||
Core client routes, not module ones:
|
||||
|
||||
| Path | Page |
|
||||
@@ -820,18 +825,34 @@ named for a *place* and never for a meaning):
|
||||
| `team.overview` | the Team overview page, below the counts | `{ teamId, externalId, moduleId }` |
|
||||
| `team.member.row` | each roster row, trailing cell | `{ displayName, isLeader, linked }` |
|
||||
|
||||
> **Amended 2026-08-17 (phase 3).** `team.member.row` was specified with
|
||||
> `{ memberKey, userId, displayName }`, which §3.2 forbids: a slot component runs in the browser, so
|
||||
> those props can only reach it by being published in the roster response to every visitor. The two
|
||||
> identifiers are dropped. `team.overview`'s three are kept — a core row id, a game-side group id and
|
||||
> a module name name no person — and they are served on `GET /teams/:slug` only, not on the index,
|
||||
> which has no slot to feed.
|
||||
|
||||
Both unfilled on bare core, which renders exactly the page core writes. Neither is typed by content —
|
||||
`team.overview` is "the spot under the counts", not "where the game puts guild stats".
|
||||
> **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:
|
||||
>
|
||||
> | 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 }` |
|
||||
>
|
||||
> 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,
|
||||
> which matters here because the thing being contained is CORE's content failing inside the MODULE's
|
||||
> page.
|
||||
>
|
||||
> The props are the module's own vocabulary. `memberKey`/`userId` are not among them and could not
|
||||
> be: §3.2 withholds both from every public roster, and a client slot only receives what the browser
|
||||
> was already sent.
|
||||
|
||||
### 3.5 Nav
|
||||
|
||||
> **Superseded 2026-08-17 (phase 3, org lead).** None of the three entries below is registered, and
|
||||
> the `teams` feature flag is not either. Core publishes no Team nav row because a core row would name
|
||||
> a surface core does not own, sitting beside the module's own row for the same thing in a different
|
||||
> word. `/admin/teams`'s sidebar entry, which landed in phase 2 and is an operator view of the
|
||||
> primitive, is unaffected and stays.
|
||||
|
||||
One coded public header entry, `{ label: 'Teams', to: '/teams' }`, plus `{ label: 'My Teams',
|
||||
to: '/player/teams' }` in the player portal and `{ label: 'Teams', to: '/admin/teams', group:
|
||||
'Community' }` in the admin sidebar. All three flow through the existing registered-defaults → admin
|
||||
@@ -2069,18 +2090,32 @@ guild called "Admin" cannot put an official-looking page on the site.
|
||||
|
||||
### Phase 3 — Team pages, roster, nav, activity feed (`website` + `module-uo`)
|
||||
|
||||
> **Amended 2026-08-17, while building this.** Five corrections, all found by building or testing the
|
||||
> thing described below.
|
||||
> **Amended 2026-08-17, while building this.** Six corrections. The first is the org lead's, and it
|
||||
> changes what this phase ships; the rest were found by building the thing described below.
|
||||
>
|
||||
> **§3.2 and §3.4 contradict each other, and §3.2 wins.** §3.2 says a member key is a game-internal
|
||||
> identifier and a user id names a site account, and that **neither is published**; §3.4 then declares
|
||||
> the `team.member.row` slot with props `{ memberKey, userId, displayName }`. A client-side slot can
|
||||
> only receive what the browser was sent, so honouring §3.4 means putting both identifiers into every
|
||||
> public roster response — for every visitor, module installed or not. **Settled: the slot is declared
|
||||
> with `{ displayName, isLeader, linked }`.** module-uo leaves it unfilled, because the useful thing to
|
||||
> put there is a link to the character behind a row and these props do not identify one; filling it
|
||||
> with a guess from a display name is worse than an empty cell. A future phase that wants this back
|
||||
> needs an opaque per-response row token, not the raw key.
|
||||
> **THERE IS NO CORE TEAM SURFACE. Teams is a contract primitive, not a page.** §3.1 puts `/teams`,
|
||||
> `/teams/:slug`, `/teams/:slug/roster` and `/player/teams` in core and §3.5 registers three core nav
|
||||
> entries for them. **Settled (org lead): all seven are dropped.** Core does not own the word for a
|
||||
> Team — a UO shard calls them guilds, and the Rust module that comes next will call them clans — so a
|
||||
> core page under a noun core invented would sit beside `module-uo`'s existing `/uo/guilds` saying the
|
||||
> same thing twice, in the wrong vocabulary. Core keeps the tables, the sync, the access resolver, the
|
||||
> activity feed and the whole API; the **module** builds the pages on that contract. `/admin/teams`
|
||||
> stays: an operator inspecting the primitive is looking at the primitive.
|
||||
>
|
||||
> **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.
|
||||
>
|
||||
> **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
|
||||
> up storing them. The module id is matched rather than trusted — an external id is unique only within
|
||||
> a module.
|
||||
>
|
||||
> **§3.3's projection is an EIGHTH `MODULE_API` member and 1.6.0's list said seven.** Settled by the
|
||||
> org lead: **1.6.0 is amended in place** rather than bumped, applying the same rule Protocol 4 got in
|
||||
@@ -2091,13 +2126,9 @@ guild called "Admin" cannot put an official-looking page on the site.
|
||||
> rung system could not be consulted* are opposite situations: the first is withholding nothing and
|
||||
> must serve the roster whole, the second must serve none of it. The refusal therefore carries
|
||||
> `projects` — `false` for "there is no audience model here", `true` for "there is one and I could not
|
||||
> ask it" — and only the second fails closed. Without that split, bare core serves an empty roster on
|
||||
> every Team page.
|
||||
>
|
||||
> **The module answers with member KEYS, not rows.** §3.3's "the module returns the rows it permits"
|
||||
> would let a module widen what is published by handing back a `userId` core had withheld, and core's
|
||||
> field guarantee would then rest on every module's good behaviour. Core asks which rows, keeps what a
|
||||
> row looks like, and re-normalises whatever comes back through its own public shape.
|
||||
> ask it" — and only the second fails closed. Also, the module answers with member **keys**, not rows:
|
||||
> returning rows would let a module widen what is published by handing back a `userId` core had
|
||||
> withheld, leaving core's field guarantee resting on every module's good behaviour.
|
||||
>
|
||||
> **Core's five activity kinds are four here.** `core.forum.thread` has nothing to emit it until the
|
||||
> forum lands in phase 4. Separately, and not in the doc at all: **the first roster for a Team emits
|
||||
|
||||
Reference in New Issue
Block a user