docs(teams): Team core, MODULE_API 1.6.0, and what phase 2 disproved #154

Merged
whitlocktech merged 1 commits from feat/teams-phase2-team-core into edge 2026-08-17 22:20:00 +00:00
Member

Teams Phase 2, docs half. Targets edge.

Pairs with website #151 and Module-uo #9.

MODULE_API.md — 1.6.0

The Team surface becomes contract. Additions only, so minor; module-uo's coreApi: "^1.3.0" still resolves.

Per your decision, one 1.6.0 covers all seven additions rather than a minor per phase. The consequence is that the document describes members that do not work yet, so each carries the phase it arrives in, and the two that cannot function — ctx.teams.activity.push (phase 3) and api.registerSlashCommands (phase 7) — are documented as present and throwing, 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.

registerTeamProvider gets the fullest treatment, because it is the first registration where core calls the module and waits. The envelope, the 10-second budget and the refusal semantics are contract rather than 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 exist specifically to stop a future reader "fixing" them: why team_forum_grants does not use the obvious generated column, and why two columns TEAMS.md never mentions have to exist.

TEAMS.md — five amendments

Marked inline as amendments with their date, rather than silently rewriting the plan. Each was found by building or testing the thing the plan describes:

  • §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 §2.10 exists to prevent. §2.10 wins.
  • 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 contemplateroster_synced_at and members_empty_since, both serving §2.4's gates, which per-module sync state cannot express.
  • 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.
  • §2.8.2's matcher needed two narrow widenings, both real impersonation vectors: plurals ("Guild of Moderators") and punctuation-split acronyms ("G.M."). Neither re-admits substring matching.

One thing recorded but not acted on, in Module-uo #9 rather than here: §2.5's multiple leaders are unreachable for UO, because Protocol 4 does not carry guild rank on the wire. That is a protocol change if it is wanted.


AI-assisted: written with Claude Code. Commits carry Co-Authored-By: Claude <noreply@anthropic.com>.

Teams **Phase 2**, docs half. Targets `edge`. Pairs with website #151 and Module-uo #9. ## `MODULE_API.md` — 1.6.0 The Team surface becomes contract. Additions only, so minor; module-uo's `coreApi: "^1.3.0"` still resolves. Per your decision, **one 1.6.0 covers all seven additions** rather than a minor per phase. The consequence is that the document describes members that do not work yet, so each carries the phase it arrives in, and the two that cannot function — `ctx.teams.activity.push` (phase 3) and `api.registerSlashCommands` (phase 7) — are documented as **present and throwing**, 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. `registerTeamProvider` gets the fullest treatment, because it is the first registration where core calls the **module** and waits. The envelope, the 10-second budget and the refusal semantics are contract rather than 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 exist specifically to stop a future reader "fixing" them: why `team_forum_grants` does not use the obvious generated column, and why two columns TEAMS.md never mentions have to exist. ## `TEAMS.md` — five amendments Marked inline as amendments with their date, rather than silently rewriting the plan. Each was found by building or testing the thing the plan describes: - **§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` §2.10 exists to prevent. §2.10 wins. - **`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** — `roster_synced_at` and `members_empty_since`, both serving §2.4's gates, which per-module sync state cannot express. - **`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. - **§2.8.2's matcher needed two narrow widenings**, both real impersonation vectors: plurals ("Guild of Moderators") and punctuation-split acronyms ("G.M."). Neither re-admits substring matching. One thing recorded but **not** acted on, in Module-uo #9 rather than here: §2.5's multiple leaders are unreachable for UO, because Protocol 4 does not carry guild rank on the wire. That is a protocol change if it is wanted. --- AI-assisted: written with Claude Code. Commits carry `Co-Authored-By: Claude <noreply@anthropic.com>`.
wtclaude added 1 commit 2026-08-17 20:34:02 +00:00
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>
whitlocktech merged commit e68affbcf5 into edge 2026-08-17 22:20:00 +00:00
whitlocktech deleted branch feat/teams-phase2-team-core 2026-08-17 22:20:01 +00:00
Author
Member

Correction: the last line of this description is now out of date

I closed with "§2.5's multiple leaders are unreachable for UO, because Protocol 4 does not carry guild rank on the wire. That is a protocol change if it is wanted."

It was wanted, and it is not a protocol change in the versioning sense: Protocol 4 is on edge and unreleased, so it has been amended in place with no bump — a bump is owed only once a protocol reaches main.

Roster members now carry rank, and getTeamLeaders() returns every member at rank 4:

  • servuo-plugins #13 — the emitter
  • Module-uo #10 — the ingest and the provider
  • docs #155v4.md §2.3 and INTEGRATION.md

Nothing in this PR changes. TEAMS.md never asserted the limitation — it was only in this description and in Module-uo #9's — so there is no amendment here to correct. §2.5's multiple leaders now simply work as written.

One thing #155 adds that is worth knowing about while reviewing this: PlayerMobile.GuildRank reports Leader for anyone at GameMaster or above whatever their real rank, so the bridge omits the rank entirely for staff. An absent rank means "not known" and must be read as neither 0 nor leadership — otherwise a staff member appears as a guild leader on a public page, which is the §2.8 impersonation concern arriving by a different door.

## Correction: the last line of this description is now out of date I closed with *"§2.5's multiple leaders are unreachable for UO, because Protocol 4 does not carry guild rank on the wire. That is a protocol change if it is wanted."* It was wanted, and it is not a protocol *change* in the versioning sense: Protocol 4 is on `edge` and unreleased, so it has been **amended in place with no bump** — a bump is owed only once a protocol reaches `main`. Roster members now carry `rank`, and `getTeamLeaders()` returns every member at rank 4: - servuo-plugins #13 — the emitter - Module-uo #10 — the ingest and the provider - docs #155 — `v4.md` §2.3 and `INTEGRATION.md` **Nothing in this PR changes.** `TEAMS.md` never asserted the limitation — it was only in this description and in Module-uo #9's — so there is no amendment here to correct. §2.5's multiple leaders now simply work as written. One thing #155 adds that is worth knowing about while reviewing this: `PlayerMobile.GuildRank` reports Leader for anyone at GameMaster or above whatever their real rank, so the bridge omits the rank entirely for staff. An absent rank means "not known" and must be read as neither 0 nor leadership — otherwise a staff member appears as a guild leader on a public page, which is the §2.8 impersonation concern arriving by a different door.
Sign in to join this conversation.
No description provided.