docs(teams): the Teams bet, as built (Teams cutover 6/6) #169
Reference in New Issue
Block a user
No description provided.
Delete Branch "edge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Cutover 6 of 6, and the last: the documentation catching up with the five repos that already have.
What lands
website/TEAMS.md— the design of record, plus an "as built" amendment on every phase. Those amendments are most of the value in this PR: each one records what the build disproved about the design, and several are corrections the design was wrong about rather than details it omitted. The largest is phase 3's — Teams is a contract, not a surface — which deleted core's Team pages and inverted the extension slots. Phase 10 is cancelled and says so, with the argument for it kept because a future phase would start from it.website/MODULE_API.md—MODULE_API_VERSION1.6.0:registerTeamProviderand its five members,ctx.teams.*,registerSlashCommands,registry.declareModuleSlot(id, name, { core })andSlotas the kit's ninth member. Three amendments in place, all made while 1.6.0 wasedge-only.link/v4.md— the protocol 4 spec:guild.roster,guild.leave, per-member rank.website/BACKEND_DESIGN.md— the ~15 Team tables, the routes and the security boundaries.modules/rust-dryrun.md— revisited for Teams, since the integration kit sends a reader there first.Note
RunicGateway/docs#168(the automatedwebsite/PROJECT_TREE.mdsync, fired by the website cutover) targetsmainindependently. If it merges first this may need a trivial rebase on that one file.AI disclosure
Written with Claude Code (Opus 5). Commits carry the
Co-Authored-Bytrailer.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>TEAMS.md gains a dated amendment on phase 3 with five corrections, all found by building the thing it describes: - §3.2 and §3.4 contradict each other about `team.member.row`'s props, and §3.2 wins because it is the security rule. A client slot can only receive what the browser was sent, so §3.4's `{ memberKey, userId, displayName }` means publishing both identifiers in every public roster, module installed or not. The slot is redeclared with what core can honestly supply. - §3.3's projection is an EIGHTH MODULE_API member where 1.6.0 listed seven. Settled by the org lead: 1.6.0 is amended in place, on the rule Protocol 4 was given in phase 2 — a contract owes a bump only once it has reached `main`. - "the module declines" needed splitting in two before it could be built. No module at all withholds nothing and must serve the roster whole; a module whose rungs could not be consulted must serve none of it. Only the second fails closed, or bare core shows an empty roster on every Team page. - the module answers with member KEYS, not rows, so it can narrow what is published and cannot widen it. - core's five activity kinds are four until the forum lands, and a Team's FIRST roster emits no join items at all. §2.11's route table gains the activity endpoint it never had, and MODULE_API.md documents `projectRoster`, the inverted fail-closed semantics that make it different from every other provider call, and `ctx.teams.activity.push`'s item shape and its four contractual properties. BACKEND_DESIGN.md: the seventh Team table, its retention, and the three public routes' new behaviour — `enabled` on the index, the slot props on the single Team, the per-caller row projection on the roster, and the feed. Co-Authored-By: Claude <noreply@anthropic.com>Amends MODULE_API 1.6.0 in place - it has only ever been on edge, the same rule the eighth and ninth members were given - and it is a correction rather than an addition. As first written, the inverted slot direction had core fill three literal uo.guild.* names. That worked for module-uo and silently did nothing for anyone 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 string literals the Sec 5.2 checker masks by construction. Sec 3.7a now documents declareModuleSlot(id, name, { core }) and the three contributions core offers - team.activity, team.forum, team.notify - as a table, with the rules that follow from the direction: the member is optional, a slot that asks for nothing stays empty, more than one slot may ask for the same contribution, and asking for one core does not offer THROWS at the declaration rather than rendering empty forever. TEAMS.md's two accounts of the inversion (Part 3's supersession note and the phase 3 amendment) say the same thing. Also corrects the UI kit's count in Sec 3.4 and Sec 3.7a: Slot made it nine in phase 3 and three places still said eight. Found by phase 11 while writing the chapter that teaches this shape to an audience outside this org. Co-Authored-By: Claude <noreply@anthropic.com>The kit's README sends a reader here FIRST - it is the shortest honest picture of the whole job - and it predated Teams, so it taught a second game to build its teams as private module data and never mentioned the provider. The two places the contract changed since it was written are now in it, and nothing else moved: all four findings stand, including the identity gap, which is still the one a real second module hits first. What Rust adds that UO does not, and why it was worth revisiting rather than noting: - externalId must survive a rename and a Rust team HAS no name - it is a numeric team id in the save. The right answer, and the one a designer is least likely to reach for. - `complete` is per SERVER, not per community. Six servers are six team spaces, so a provider that can reach five must leave `complete` off or core archives every team on the sixth. - A wipe empties every team, so { ok: true, complete: true, teams: [] } is TRUE once a month and core archiving all of them is correct - which is exactly why an unreachable RCON must answer { ok: false } instead. The two states are one API call apart and only the module can tell them apart. - The team route carries a server id as well as a team id, so the external id is <serverId>:<teamId>. Core stores that and never parses it; an external id is opaque to core by design, and this is the case that shows why. Also: rust_teams stays the module's table and core's teams stays core's, which is the boundary worth stating in the one document where both appear; and the kit is nine members now, not seven. Co-Authored-By: Claude <noreply@anthropic.com>