docs(teams): phase 6 as built — four deviations and a ninth contract member
Part 6 gains an as-built header rather than a rewrite, so the reasoning that produced the original design stays legible beside what the build learned. Four deviations. There was no web notification settings screen to add the Team list to — `/auth/me/notifications/*` was built for the app in M7 and had zero web consumers, which is survivable for push and not for a sink whose whole argument is the web-only user. Email defaults to `off` rather than `digest`, on the org lead's call: digest-by-default would start mailing every member of every Team the moment an operator connects Gmail. Roster events tickle but do not email. And a ninth member joined MODULE_API 1.6.0. `pageUrlTemplate` is the member, and it exists because phase 3 left core with no Team page and therefore no way to link to one. It joins 1.6.0 in place under the rule set 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`. Two further build decisions are recorded where they belong: the digest computes at send time and keeps no queue (§6.4), and one-click unsubscribe is a stateless HMAC whose whole capability is muting one (user, Team) pair (§6.4). BACKEND_DESIGN gains the table, the two `/auth/me` routes and the unsubscribe endpoint — the only write in the public tier and the only route with no `siteMode`, because the mail went out before the site went into maintenance. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -33,14 +33,21 @@ The client half carries the same number (`client/src/modules/version.js`) and a
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.6.0 — Teams, the whole surface.** Eight additions, no removals and no changed signature, so minor;
|
||||
**1.6.0 — Teams, the whole surface.** Nine 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 provider's optional `projectRoster` and `pageUrlTemplate` · `api.registerSlashCommands(...)` ·
|
||||
`registry.declareModuleSlot(...)` with `Slot` in the UI kit.
|
||||
|
||||
> **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,
|
||||
> `pageUrlTemplate` on the team provider, joins 1.6.0 in place — same rule as the eighth below, and
|
||||
> 1.6.0 is still `edge`-only. It is the one thing phase 6 found that the design of record had not
|
||||
> anticipated: after phase 3 deleted core's Team pages, nothing in this contract could tell core where
|
||||
> a Team page actually is, so a notification email could name a Team and not link to it. See
|
||||
> `registerTeamProvider` below.
|
||||
>
|
||||
> **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.
|
||||
@@ -395,7 +402,8 @@ Before it existed, core's post controller required `utils/newsGump` directly —
|
||||
naming a UO file, and the last thing binding core to the module.
|
||||
|
||||
**`registerTeamProvider({ getTeams, getTeamMembers, getTeamLeaders })`** — added in API 1.6.0. The
|
||||
module becomes the authoritative source of Teams for this deployment.
|
||||
module becomes the authoritative source of Teams for this deployment. Two further members,
|
||||
`projectRoster` and `pageUrlTemplate`, are optional and documented below.
|
||||
|
||||
**One provider per deployment.** Unlike every other registry, this holds a single value: Teams have
|
||||
one authoritative source by construction, and two modules answering "what Teams exist" would produce
|
||||
@@ -410,6 +418,8 @@ getTeamMembers(externalId) // (string) => Promise<{ ok, complete?, members }>
|
||||
getTeamLeaders(externalId) // (string) => Promise<{ ok, leaders }> // leaders = [memberKey]
|
||||
projectRoster(externalId, members, viewer) // OPTIONAL (1.6.0, phase 3)
|
||||
// => Promise<{ ok, members }> // members = [memberKey]
|
||||
pageUrlTemplate // OPTIONAL (1.6.0, phase 6) — DATA, not a method
|
||||
// e.g. '/uo/guilds/{externalId}'
|
||||
|
||||
// authoritative
|
||||
{ ok: true, complete: true, teams: [ { externalId, name, abbr?, meta? } ] }
|
||||
@@ -447,6 +457,27 @@ Core distinguishes two refusals, and a module does not have to do anything to ge
|
||||
serves an empty roster and says so in the response (`projected: false`,
|
||||
`projectionUnavailable: true`).
|
||||
|
||||
**`pageUrlTemplate` is the fifth member, it is data rather than a method, and it exists because core
|
||||
cannot link to a Team page.** Teams are a contract primitive with **no core surface** (TEAMS.md
|
||||
Part 3): core owns the tables, the sync and the access rules, and the module that owns the vocabulary
|
||||
owns the page. That is settled and right, and it leaves core unable to write the link a notification
|
||||
email needs — an email about a forum reply that cannot take you to the thread is most of the way to
|
||||
useless. So the module that owns the page says where it is.
|
||||
|
||||
```js
|
||||
api.registerTeamProvider({ getTeams, getTeamMembers, getTeamLeaders,
|
||||
pageUrlTemplate: '/uo/guilds/{externalId}' })
|
||||
```
|
||||
|
||||
Core substitutes `{externalId}` and `{slug}` and does nothing else with it. **A relative path only** —
|
||||
a template naming its own host is refused at registration, since there is no reason for a module to
|
||||
redirect the site's outbound mail, and a protocol-relative `//host/x` is refused with it. Omitting the
|
||||
member costs the deployment clickable links in Team notification email and nothing else.
|
||||
|
||||
**Data rather than a callback, deliberately.** A function here would put a module hook on the mail
|
||||
path — one more thing that can hang or throw between a forum reply and the mail about it — to produce
|
||||
a string that never varies.
|
||||
|
||||
Core hands over the roster rows it holds plus a described viewer — `{ userId, role }`, or `null` for
|
||||
an anonymous caller — and never the `users` row, which would make every column of that table part of
|
||||
this contract. **The module answers with member KEYS, not rows.** Core keeps ownership of what a
|
||||
@@ -1201,19 +1232,23 @@ forced it is worth stating because it will recur:
|
||||
|
||||
```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 page, from the UI kit:
|
||||
<Slot name="uo.guild.header" externalId={guildId} moduleId="uo" />
|
||||
<Slot name="uo.guild.detail" externalId={guildId} moduleId="uo" />
|
||||
<Slot name="uo.guild.forum" externalId={guildId} moduleId="uo" />
|
||||
```
|
||||
|
||||
**A module declares one slot per PLACE, not one per page.** `module-uo` declares two on the same guild
|
||||
page — core fills the first with the Team activity feed and the second with the Team forum — because a
|
||||
slot holds one component and the first fill wins. Collapsing them into one would hand core the
|
||||
decision about where each of its contributions sits, on a page the module owns. Two also keeps them
|
||||
independent: a deployment with the forum switched off renders the feed unchanged.
|
||||
**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
|
||||
decision about where each of its contributions sits, on a page the module owns, and the module does
|
||||
use that freedom: the notification control goes **above** the roster because muting is an action *on*
|
||||
the page, and the other two go below it because they are content *in* it. Separate slots also keep
|
||||
them independent: a deployment with the forum switched off renders the other two unchanged.
|
||||
|
||||
**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
|
||||
|
||||
Reference in New Issue
Block a user