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:
@@ -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`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user