docs(teams): Team core, MODULE_API 1.6.0, and what phase 2 disproved
Documents phase 2 of docs/website/TEAMS.md across the three files that had to
change, and records the five places building it disagreed with the design.
## MODULE_API.md — 1.6.0
The Team surface becomes contract: `api.registerTeamProvider(...)`,
`ctx.teams.publish` / `ctx.teams.reconcile` / `ctx.teams.activity.push`,
`api.registerSlashCommands(...)`, and the two client slots. Additions only, so
minor; module-uo's `coreApi: "^1.3.0"` still resolves.
Per the org lead's decision, one 1.6.0 covers the whole surface rather than a
minor per phase -- so the document names the phase against each member, and the
two that cannot work yet are marked as present-and-throwing rather than left to
be discovered at runtime.
`registerTeamProvider` gets the fullest treatment because it is the first
registration where core calls the MODULE and waits for an answer. The envelope,
the 10-second budget and the refusal semantics are all contract, not
implementation: they are how a module says "I cannot answer" without core hearing
"there is nothing". `ctx.teams` is documented as push-only, with the reason there
is no reader — a module answers questions about Teams, it does not ask them.
## BACKEND_DESIGN.md
The six Team tables, the rename rule, the active-only uniqueness encoding, the
per-column account-deletion decisions, and all eighteen routes across the three
tier tables.
Two entries there exist to stop a future reader "fixing" them: why
`team_forum_grants` does not use the obvious generated column, and why the two
columns TEAMS.md never mentioned have to exist.
## TEAMS.md — five amendments, marked as amendments with their date
- **§2.5's SQL and §2.10's decision cannot both hold.** MariaDB refuses ON
DELETE SET NULL on a base column of a stored generated column (1901), so
§2.5's `active_user` forces the CASCADE that §2.10 exists to prevent. §2.10
wins; the marker is re-encoded for identical semantics.
- **`team_forum_grants` lands in phase 2**, so the four-path resolver is written
once and its non-contamination tests are real.
- **Two columns the document did not contemplate**, both serving §2.4's gates:
`roster_synced_at`, because sync state is per MODULE and gate 3 leaves one
Team behind while the others sync; and `members_empty_since`, gate 4's
per-Team quarantine.
- **`leader` on the member shape is not path 2.** Taking §2.3 and §2.5 both
literally gives one column two writers, and the roster writes first — so a
refused `getTeamLeaders()` silently demoted everyone. Found by its own test.
- **§2.8.2's matcher needed two narrow widenings**, both real impersonation
vectors the whole-word rule missed: a term matches a name word's singular
("Guild of Moderators"), and a run of single-letter words is compared joined
("G.M."). Neither re-admits substring matching.
Pairs with website (Teams phase 2) and Module-uo (the provider).
Refs docs/website/TEAMS.md Part 12 phase 2
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1888,6 +1888,14 @@ this protocol.
|
||||
|
||||
## Part 11 — `MODULE_API_VERSION` bump proposal
|
||||
|
||||
> **Amended 2026-08-17, on the org lead's decision.** The seven additions below land under **one**
|
||||
> 1.6.0, declared in phase 2, rather than a minor bump per phase. `MODULE_API.md` therefore documents
|
||||
> members before they work, so each is marked with the phase that implements it, and the two that do
|
||||
> not yet — `ctx.teams.activity.push` (§4, phase 3) and `api.registerSlashCommands` (§7.1, phase 7) —
|
||||
> are **present and throw** with an error naming that phase. Present rather than absent so a module
|
||||
> written against the published version fails at registration with an explanation, instead of at
|
||||
> whatever moment someone first exercises the feature.
|
||||
|
||||
**1.6.0 — minor.** Every change is an addition; no member is removed and no existing signature changes,
|
||||
so `MODULE_API.md` §1.1's table gives minor, and `module-uo`'s `coreApi: "^1.3.0"` still resolves.
|
||||
|
||||
@@ -1991,6 +1999,40 @@ line with a continuation flag for the pathological guild.
|
||||
|
||||
### Phase 2 — Team core (`website` + `module-uo` + `docs`)
|
||||
|
||||
> **Amended 2026-08-17, while building this.** Five corrections, all found by building or testing the
|
||||
> thing described below.
|
||||
>
|
||||
> **§2.5's SQL and §2.10's decision cannot both hold as written.** §2.5 gives `team_forum_grants` a
|
||||
> generated column `active_user AS (IF(revoked_at IS NULL, user_id, NULL))` and a `CASCADE` foreign
|
||||
> key; §2.10 later settles that key as `SET NULL` so the audit trail survives an account deletion.
|
||||
> MariaDB refuses `ON DELETE SET NULL` on a foreign key whose column is a base column of a STORED
|
||||
> generated column (error 1901), so the generated column forces the `CASCADE` — and with it, the loss
|
||||
> §2.10 exists to prevent. **Settled: §2.10 wins.** The marker is derived from `revoked_at` alone and
|
||||
> `user_id` moves into the unique KEY, which gives identical semantics — at most one active grant per
|
||||
> (team, user), unlimited revoked rows — with `user_id` free to be `SET NULL`.
|
||||
>
|
||||
> **`team_forum_grants` is created in this phase**, not in phase 4, so the four-path resolver is
|
||||
> written once and its non-contamination tests are real. Nothing writes it yet; the grant flow, the
|
||||
> per-Team cap and the leader UI stay phase 4's.
|
||||
>
|
||||
> **Two columns on `teams` that this document did not contemplate**, both serving §2.4's gates.
|
||||
> `roster_synced_at`: `team_sync_state` holds one row per *module*, and gate 3 leaves one Team's roster
|
||||
> untouched while the others sync — without a per-Team stamp that Team's page would report the
|
||||
> module's last success as its own, which is exactly the staleness the gate exists to surface.
|
||||
> `members_empty_since`: gate 4's per-Team quarantine, the twin of `pending_empty_since`.
|
||||
>
|
||||
> **`leader` on the member shape is not path 2.** §2.3 puts `leader` on a member and §2.5 says the sync
|
||||
> writes `is_leader` from `getTeamLeaders()`; taking both literally gives one column two writers, and
|
||||
> the roster's write lands *first* — so a refused `getTeamLeaders()` silently demoted everyone. The
|
||||
> roster now **seeds** `is_leader` on insert only, so a Team is not leaderless while that call is
|
||||
> failing, and `getTeamLeaders()` alone moves it afterwards.
|
||||
>
|
||||
> **The §2.8.2 matcher needed two narrow widenings**, both real impersonation vectors the whole-word
|
||||
> rule missed: a single-word term also matches a name word's singular ("Guild of Moderators"), and a
|
||||
> run of two or more single-letter words is also compared joined ("G.M."). Neither re-admits substring
|
||||
> matching — only a trailing `s` off the *whole* term is stripped, and the join is of single letters,
|
||||
> never of the whole name.
|
||||
|
||||
`teams`, `team_members`, `team_sync_state`; `registerTeamProvider` + the three `ctx.teams` members
|
||||
(**`MODULE_API_VERSION` → 1.6.0**); the reconciler with all four refusal gates; the four-path
|
||||
resolver with its non-contamination tests; `team_leader_overrides`; the public/player/admin read API;
|
||||
|
||||
Reference in New Issue
Block a user