docs(teams): phase 7 as built — five amendments to §7.1

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>
This commit is contained in:
2026-08-18 18:53:56 -05:00
parent 30d964c090
commit c196d03d31
3 changed files with 130 additions and 18 deletions

View File

@@ -100,11 +100,14 @@ module fills with anything live. Core does not grow an SSE stack for this.
exactly two shared-secret HTTP channels:
- **app → bot**, `utils/botInternalClient.js``bot/src/internal/internal.routes.js`
(`/internal/config`, `/internal/status`, `/internal/announce`, `/internal/mod-reverse`), 4s timeout,
never throws, always returns `{ ok, status, data, error }`.
(`/internal/config`, `/internal/status`, `/internal/announce`, `/internal/mod-reverse`, and since
phase 7 `/internal/refresh-commands`), 4s timeout, never throws, always returns
`{ ok, status, data, error }`.
- **bot → app**, `SITE_INTERNAL_URL=http://app:3001/internal/bot-config` on the app's *unpublished*
internal listener (`server/src/internalApp.js`), with a retry-with-backoff bootstrap so a bot
restart self-heals.
restart self-heals. Phase 7 added `bot/src/site/appInternalClient.js` for `/internal/commands` and
`/internal/commands/dispatch` on that same listener — it derives the base from `SITE_INTERNAL_URL`'s
origin rather than taking a second variable naming the same host.
Slash commands are registered from a static array (`bot/src/discord/commands/index.js`) and pushed
with `REST.put(Routes.applicationGuildCommands(...))` on ready (`discordManager.js:25`) — a **whole-set
@@ -1669,6 +1672,42 @@ is already being built there.
### 7.1 Slash-command registration
> **Amended 2026-08-18 (phase 7), as built.** Five changes, four of them forced by what the tree
> already looked like.
>
> **The example command is `/guild`, registered by module-uo, and core registers none.** §7.1 wrote
> `/team` as a core command against core's own Team rows. Phase 3 settled that **Teams is a contract
> primitive with no core surface** — core does not own the word for a Team, which is why four core
> Team pages were deleted — and a core `/team` publishes that same invented noun into a channel. The
> module owns the vocabulary, so the module owns the command. Core ships the dispatcher, the actor
> resolver and the transport, and zero commands.
>
> **The deep link comes from `pageUrlTemplate`, because `/teams/:slug` does not exist.** The snippet
> below still says `${siteBaseUrl}/teams/${slug}`; there is no such page. A handler builds its own
> link — module-uo's is `/uo/guilds/{externalId}` — which is the same hole phase 6 found in the mail
> path and closed with the ninth contract member.
>
> **The re-register nudge is its own endpoint, `POST /internal/refresh-commands` on the bot**, not a
> ride on `/internal/config`. That body carries the DECRYPTED bot token: telling the bot that a
> module changed should not require reading a secret out of the database to say it.
>
> **`actor` carries `role` as well as `isStaff`.** The two answer different questions and a boolean
> loses one — `isStaff` is core's gate for `access: 'staff'`, `role` is what a module with its own
> audience rungs needs to place the caller on them. It is the pair `projectRoster`'s viewer already
> carried (§3.3), not a new class of disclosure.
>
> **Deregistration needed a second half this section did not consider.** "A module that is gone is
> simply absent from the next pull" holds across the restart an uninstall asks for. It does not hold
> for the runtime toggle: the registries have no removal path, so a module an operator disables would
> keep a live handler behind a command Discord still advertises. Liveness is therefore asked at both
> the pull and the dispatch, and a disabled owner's command answers `unknown`.
>
> The envelope also gained **`notice`** — a private aside delivered beside a public answer, which is
> how §9 answer 5's "public projection plus an ephemeral prompt to link" is actually expressible: one
> reply cannot be both public and ephemeral, and that it becomes a follow-up is the platform's
> decision, not the handler's.
**Ownership, decided:** the registrant owns the **definition and the handler**; the handler runs **in
the website process** and returns a **response envelope**; the **bot owns every Discord-specific
concern** — deferral, the 3-second ack, ephemerality, follow-ups, interaction tokens, embeds. This is
@@ -2485,13 +2524,24 @@ them as toggles automatically, but nothing here builds a Team screen or a deep-l
app, so a Team tickle on mobile opens the app and no more. That is a stated limitation, not an
oversight.
### Phase 7 — Discord: slash commands (`website` + `bot` + `docs`)
### Phase 7 — Discord: slash commands (`website` + `module-uo` + `docs`) — **DONE 2026-08-18**
`api.registerSlashCommands`, `/internal/commands` + `/internal/commands/dispatch`, the bot's
defer→dispatch→edit path, the actor resolver, the version-bump re-register, and `/team` as the first
command through it.
defer→dispatch→edit path, the actor resolver, the version-bump re-register, and the first command
through it.
**Ships:** a working `/team`, and the seam a module needs for its own commands.
**Ships:** a working `/guild`, and the seam a module needs for its own commands.
**THREE repos, not the plan's `website` + `bot` + `docs` — `bot` is not a repo.** It is a workspace
inside `website`, so the bot half lands in the same PR as the server half; `module-uo` joins instead,
because the command that proves the seam belongs to the module and not to core (see the amendment at
the head of [§7.1](#71-slash-command-registration)).
**The bot got its first test harness.** It had no `test` script and no tests at all — CI ran
`npm ci --prefix bot` and nothing else — which was defensible while the bot only wired up its own
static commands. It is not defensible now that it merges a pulled set into a single all-or-nothing
registration and runs the interaction path, and phases 8 and 9 add more. `bot/test/` and a
`bot-tests` job replace `bot-install`.
### Phase 8 — Discord: notifications bridge (`website` + `bot`)