The org lead cancelled the capability layer on 2026-08-19, deferring it until a
second integration is wanted or it is asked for by name. Phase 11 is now the last
phase of the bet.
The same argument that put phase 10 last is the argument for not doing it yet:
with one integration built, the refactor would extract a capability surface from a
single implementation and have nothing to check the extraction against. It is
cheaper and better-informed the day a second platform exists, because that
platform is what proves which of the five capabilities the seam needs.
Three places pointed forward at it and now say what is true instead:
- Sec 7.2 and Sec 7.3 both justify the Admin -> Teams panels by "phase 10 makes
the platform a registry lookup". The decision survives its reason: an operator
should not have to know which platform is configured to find the panel, and that
holds whether or not the registry is ever built.
- Sec 8.2 keeps the Matrix comparison and the capability table, with a note that
no registry is built either. The research did its job by keeping core's calls
phrased as eligibility questions rather than as Discord operations; what is
absent is the indirection, so `discord` is named directly in the bridge, the
voice provisioner and the command dispatcher.
The phase entry keeps its body rather than deleting it, because the argument for
the layer is what a future phase would start from.
Co-Authored-By: Claude <noreply@anthropic.com>
Amends `TEAMS.md` §7.3 inline, marks phase 9 done in Part 12, and adds the
`team_integrations` row to `BACKEND_DESIGN.md`'s schema table. Pairs with
**website#159**.
Co-Authored-By: Claude <noreply@anthropic.com>
Amends §7.2 inline and marks phase 8 done in Part 12; adds the
team_integration_config row to BACKEND_DESIGN.md's schema table.
Two of the amendments are things the tree disproved rather than choices:
- §7.2's DDL cannot hold its own default row. MariaDB coerces PRIMARY KEY
columns to NOT NULL, so `team_id NULL` is unrepresentable and the override
mechanism has no base case. Confirmed against a real MariaDB (error 1048).
- §7.2's visibility gate has no data source on either side and cannot have one:
the streams carry no visibility, a forum thread is members-only by
construction rather than by a column, and core cannot see a channel's
permissions. The gate becomes an attributed operator acknowledgement.
Co-Authored-By: Claude <noreply@anthropic.com>
The command that proves the seam is the MODULE's `/guild`, not core's `/team`:
§7.1 was written before phase 3 settled that Teams is a contract primitive with
no core surface, and a core `/team` publishes the same invented noun that got
core's Team pages deleted. Its deep link comes from `pageUrlTemplate` for the
same reason — `/teams/:slug` does not exist.
The re-register nudge is its own bot endpoint rather than a ride on
`/internal/config`, whose body carries the decrypted bot token. `actor` carries
`role` beside `isStaff`, since a module with its own audience rungs cannot place
a caller from a boolean. And "deregistration is free" needed a second half: it
holds across the restart an uninstall asks for, not across the runtime toggle,
so liveness is asked at both the pull and the dispatch.
MODULE_API.md stops saying `registerSlashCommands` throws and documents it —
every member of 1.6.0 is live now.
Co-Authored-By: Claude <noreply@anthropic.com>
The kit is the instruction book for putting a different game on this platform,
written for an audience outside this org. Teams expands the contract that book
teaches against, so the book is the last thing the bet owes before `edge` becomes
`main` (org lead, 2026-08-18).
One sentence in it is already wrong rather than merely incomplete.
`book/02-website-module.md` tells a reader that core declares a slot and a module
may only fill one. Phase 3 inverted exactly that, and by phase 6 module-uo declares
three — a new game's module cannot implement Teams at all without the inverted
direction.
Two shapes are genuinely new and worth teaching: the inverted slot, and
`registerTeamProvider` as the first registration where core calls the module and
waits — with the asymmetry that every call fails stale except `projectRoster`,
which fails closed, because for a visibility question "keep what you have" means
serving the roster unprojected.
The phase explicitly does NOT enumerate the contract. The kit already teaches four
members and has never mentioned notification streams, announce legs or post hooks,
all of which predate Teams. MODULE_API.md is normative; the kit teaches one path
and links out.
Its ordering is awkward and is stated rather than smoothed over: it is written
before the cutover and can only merge after it, because CI clones the pinned sha
and checks the template against that core's MODULE_API_VERSION — and 1.6.0 does
not reach `main` until the cutover lands.
Co-Authored-By: Claude <noreply@anthropic.com>
| `team_forum_moderation` | append-only, per Team, recording `actor_role` — WHICH authority was exercised. Deliberately not merged with `mod_actions`/`appeals`, which is Discord-sanction-shaped |
| `team_forum_uploads` | attribution for `uploads` mode: who uploaded what, when, how big, and to which post. Also the sweep's worklist |
| `team_notification_prefs` | per-Team notification preference (phase 6). **Opt-out for push, opt-IN for email** — `muted` defaults 0 and `email_mode` defaults `'off'`, so the two sinks default opposite ways and the asymmetry lives here rather than in a condition anyone has to remember. Team scoping lives in this table and in the recipient computation, never in a stream id. `last_digest_at` is the digest's only state and the worker is its only writer |
| `team_integration_config` | where a Team's notifications go on another platform (phase 8). One row per (platform, Team) plus a **deployment-wide default** whose `team_id` is NULL — expressed with a generated `team_key AS IFNULL(team_id, 0)` in the unique key, because a NULL cannot live in a primary key and the default row is the base case of the whole override mechanism. `members_ack` is a **precondition, not a preference**: forum posts and announcements are members-only always, core cannot see a channel's permissions, so enabling one requires an attributed operator acknowledgement that the destination is restricted — and changing the channel clears it |
| `team_integrations` | a Team's provisioned resource on another platform — today its Discord **voice channel and the role that opens it** (§7.3, phase 9). Both refs on one row because they are one lifecycle: a role for a channel that no longer exists is a badge for nowhere. `state` is core's BELIEF about the platform, never the platform's answer — the reconciler writes what it just did and the next pass re-derives the truth. A Team that stops qualifying goes to `pending_removal` with `remove_after` rather than being deleted at once, so a Team hovering around the size threshold does not delete-and-recreate its channel and change its id. `synced_at` is separate from `updated_at`, which moves whenever core writes a belief including an error |
| `content_reports` | member-raised abuse reports (phase 5). **Not a `team_*` table and not named for the forum** — `target_type` is a plain VARCHAR so a wiki page or a news comment becomes a value rather than a table. Team forum content is only the first consumer |
**Core had no user-facing report flow of any kind before `content_reports`.**`moderation`,
@@ -674,7 +676,7 @@ are authoritative, and they answer different questions:
| Artifact | Source of truth for | Generated by |
|---|---|---|
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs CORE serves.**166 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs CORE serves.**Every core URL — the public app plus the internal listener — sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
| `server/swagger/swagger-output.json` — merged into `/api/docs` | **What each core route means.** Parameters, bodies, response codes, security. | `npm run swagger`, from `#swagger.*` annotations |
Both are **core's**. An installed module's routes are in neither: they are in that module's own
> **Not built, and not scheduled.** The org lead cancelled this phase after Phase 9, deferring it
> until a second integration is actually wanted or it is asked for by name. What follows is what it
> would have done, kept because the argument for it survives its cancellation.
Refactor Phases 7–9's Discord code behind the declared-capability registry (§8.2) and prove it by
rendering the admin UI from the declaration rather than from a hardcoded "Discord" assumption. Last on
purpose: extracting a capability surface from one working implementation is honest; designing it before
one exists is speculation.
**Why cancelling it costs little.** The same argument that put it last is the argument for not doing it
yet: with exactly one integration built, the refactor would extract a capability surface from a single
implementation and have nothing to check the extraction against. §8.2's Matrix column is research, not
a second implementation, and a registry whose only consumer is the thing it was extracted from is a
layer of indirection that has not yet been paid for. The work is cheaper *and* better-informed the day
a second platform exists, because that platform is what proves which of the five capabilities the
seam actually needs.
**What it leaves behind, stated so nobody has to re-derive it.** Phases 7–9 name Discord directly —
in the bridge config (`team_discord_config`), the voice provisioner, the slash-command dispatcher and
their admin panels. That is not a defect and no code is placed differently in anticipation of a layer
that may never come. Two decisions were made *for* this phase, and both stand on their own:
the notification bridge and the voice panel live under **Admin → Teams** rather than inside the
Discord Bot panel (§7.2, §7.3), because where an operator goes to find them should not depend on which
platform fills them; and core's calls are already phrased as questions about eligibility — *these user
ids are eligible for Team 3* — rather than as instructions about overwrites. A second integration
would be a new phase against that surface, not a rescue of this one.
### Phase 11 — the integration kit (`integration-kit`) — **the last phase before the cutover**
The kit is the instruction book for putting a *different* game on this platform, written for an
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.