docs(website): add ENGAGEMENT.md, the engagement system design of record #176

Merged
whitlocktech merged 2 commits from docs/engagement-plan into edge 2026-08-29 00:07:09 +00:00
Member

Retargeted to edge (was main). Per the org lead: every phase of this workstream lands on edge, and main is touched once by the cutover. docs' edge was 16 commits behind main and 0 ahead, so it was fast-forwarded first — see §6.0a, which makes that a blocking precondition for the whole workstream.

What & why

Adds website/ENGAGEMENT.md — the design of record for an in-house, game-agnostic email and engagement system, and the next workstream after Teams. Modules declare domain events and their data contract; core owns the rules, preferences, templates and delivery. Core learns no game vocabulary.

No code, no implementation. The doc is a scoping investigation plus a phased plan; nothing starts until each phase is approved.

Contents: current-state map (notifications, email, module contract, job/scheduling infra) · gap list (25 gaps) · proposed schema additions with reasoning · the module registration mechanism · a 13-phase plan with an acceptance check, assigned documentation and named guardrails per phase · a catalogue of what the system could be used for.

Five findings that contradict the brief this started from, and shape the plan

  • There is no in-app channel. Core has exactly one sink — the content-free push tickle (utils/pushDispatch.js). No inbox table, no read API, no list surface on web or Android. The in-app channel has to be built, not adapted.
  • Email is already two-thirds of an engagement system, scoped to Teams. Gmail OAuth2 is only the transport; teamNotify.js + teamDigestWorker.js + team_notification_prefs already do recipient selection, per-user opt-in, immediate-vs-digest, HMAC unsubscribe and RFC 8058 one-click. The plan generalises that pipeline and migrates Teams onto it rather than building a second one.
  • The IDOC example's payload is not on the wire, and one field of it is not exactly knowable. house.decay carries no ownerName, nextStage or collapse estimate. ServUO runs dynamic decay (DynamicDecay.Enabled => Core.ML) and draws each stage's duration at random when that stage is entered, so a collapse time is exact only once the house is at IDOC — earlier it can only be an envelope. Separately, the current mapping fires at to == "IDOC", not at "greatly damaged", so the brief's own example does not fire today; fixing that needs no protocol change.
  • Event-name collision handling already exists. modules/registries.js's namespaced() requires an <owner>. prefix on every stream and leg, apply() throws naming the current holder, and the loader already cross-checks N modules. §7.3 records the real forward-compat note instead (a rule whose module was uninstalled must go dormant, never error or auto-delete — the call announce_job_legs already made for an orphaned leg).
  • MODULE_API_VERSION 1.6.0 is on main now, so MODULE_API.md §1.1's "1.6.0 has only ever been on edge" argument is stale. The engagement additions take a real 1.7.0 (additions only ⇒ minor; module-uo's coreApi: "^1.3.0" still resolves).

Five scope decisions, settled by the org lead and recorded at the top of the doc

  1. The in-app channel is in scope.
  2. The Teams notification pipeline is generalised and migrated onto the new system.
  3. The house.decay protocol enrichment is in scope, as a coordinated four-repo PROTOCOL_VERSION bump.
  4. Gmail OAuth2 is removed, not retained as a transport. §1.2a is the deletion inventory.
  5. The system ships with seeded working templates plus an editor, so a fresh deployment mails correctly before anyone opens the editor.

The branching model, and two blocking findings (§6.0a)

Every phase PR in every repo targets edge; main is touched exactly once, by the cutover (Phase 13). This workstream changes a wire protocol, a module API version and the mail path simultaneously, in different repos on different days — main must never hold a half-applied set.

Checked 2026-08-28, both blocking before Phase 1:

  • Every existing edge is stale — 0 ahead of main, behind by: docs 16, module-uo 9, installer 7, servuo-plugins 7, website 5, link 3. Leftovers from previous cutovers, never refreshed. Fast-forwarding is lossless; skipping it means the cutover diff carries other workstreams' leftovers. (docs is now done, as part of this PR's retarget.)
  • Three repos have no edge at allandroid-app, runicgateway.com, Integration-kit.

Also recorded: android-app/.gitea/workflows/pr-checks.yml triggers only on PRs into main, so Phase 8 lands with zero CI and the cutover is its first real build — as happened to all nine M12 phase PRs. New open question Q8 asks whether to fix the trigger or accept that deliberately.

Documentation is a phase deliverable, not an appendix (§6.0b)

A phase-by-phase table assigning the specific docs each phase owes, in docs/ and in every other repo, so nothing is deferred to a cleanup pass. §7.4 becomes the inventory that table draws from rather than a list of things to do at the end.

Two new phases

Phase 12 — runicgateway.com. Not optional polish; two of its own checks fail the build without it:

  • scripts/checkFacts.mjs fetches each fact's authority from the source repo's main, so platform.json's protocol 4→5 (Phase 10) and moduleApi 1.6.0→1.7.0 (Phase 2) go red on their own — the protocol is asserted three times over (sidecar, overlay.toml, bundle).
  • The site currently documents the opposite of what Phase 1 ships: administration/notifications-and-email.mdx carries an <Aside title="There is no SMTP option"> and "Gmail over OAuth2 is the only supported delivery path today", plus configuration.mdx:62, troubleshooting.mdx:101, system-architecture.mdx:117.
  • src/data/capabilities.mjs already claims "Web, push and email" — the web channel does not exist until Phase 7.
  • PLAY_DATA_SAFETY.md and /privacy generate from one inventory that an engagement mailer materially changes: an address used beyond account function, a send log, a suppression list. Play's Data Safety form separates "app functionality" from "communications", so this is a store-review matter, not a doc nit.

The timing is the trap: because checkFacts reads main, the site stays green through the entire edge period and breaks the instant the cutover lands. Phase 12 must be written before Phase 13 and merged in the same window.

Phase 13 — the cutover, ordered rather than per-repo-independent: docsservuo-plugins + link together (a protocol bump has three declaration sites; a main holding a v5 sidecar with a v4 overlay is a shard the installer refuses to pair) → websitemodule-uoIntegration-kit (its core-ref.json equality check is red until then, on purpose) → android-apprunicgateway.com last, because every fact it fetches must be true on main first.

Two things worth a reviewer's attention

  • G22 — the Gmail removal degrades silently. At cutover transport backfills to smtp with no credentials, so every sink politely does nothing: contact form falls back to mailto, invites surface a copyable link, resets still answer a generic 200. Nothing breaks loudly, which means email stops for the live deployment and nobody is told. Phase 1 therefore owes an admin warning, a release note and operator guidance; Gmail-as-plain-SMTP with an app password is the documented migration path.
  • G24 — an audience ceiling per trigger, and it is a security gap. shardStreams.js already filters sensitive kinds off the public push path (PUBLIC_KINDS, "defense in depth"). The engine has no equivalent, so a rule could give uo.cheat.detected or uo.audit.* a public audience — a second door through the module system's boundary. A trigger must declare the widest audience it may ever have, and a rule must not be able to widen it. Placed in Phase 2's declaration, not Phase 4.

Also adds one index row to the repo README.md.

How it was tested

Documentation only — nothing to build or run. Every claim was verified against the working trees and the live repos rather than recalled:

  • File/line references checked in website/server/src, module-uo/server, android-app/app/src/main, runicgateway.com/src.
  • MODULE_API_VERSION on main confirmed with git show main:server/src/modules/version.js (1.6.0).
  • The house.decay field set read from servuo-plugins/.../BridgeSweeps.cs:199 and link/INTEGRATION.md:207; decay semantics from the local ServUO tree (Scripts/Multis/BaseHouse.cs, Scripts/Multis/DynamicDecay.cs).
  • Protocol declaration sites confirmed at link/sidecar/src/main.rs:55 and servuo-plugins/overlay.toml:27 (both 4).
  • Every shard event kind in Part 8 taken from link/INTEGRATION.md, not invented.
  • Branch divergence measured per repo with git rev-list --left-right --count origin/main...origin/edge.
  • runicgateway.com's fact authorities read out of scripts/checkFacts.mjs (each raw(repo, path, 'main') call), and the stale Gmail/SMTP prose located by grep across src/.

Eight open questions are recorded in §7.1, each tagged with the phase it must be answered before. None block Phase 1 or Phase 2.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally. (Docs-only; no build or test surface.)
  • I have added or updated tests/docs where it makes sense. (This PR is the doc; §6.0b assigns every follow-on doc edit to the phase that causes it.)
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

This project requires disclosure of AI tool usage. Please pick one:

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
> **Retargeted to `edge`** (was `main`). Per the org lead: every phase of this workstream lands on `edge`, and `main` is touched once by the cutover. `docs`' `edge` was 16 commits behind `main` and 0 ahead, so it was fast-forwarded first — see §6.0a, which makes that a blocking precondition for the whole workstream. ## What & why Adds `website/ENGAGEMENT.md` — the design of record for an in-house, game-agnostic **email and engagement system**, and the next workstream after Teams. Modules declare domain events and their data contract; core owns the rules, preferences, templates and delivery. Core learns no game vocabulary. No code, no implementation. The doc is a scoping investigation plus a phased plan; nothing starts until each phase is approved. **Contents:** current-state map (notifications, email, module contract, job/scheduling infra) · gap list (25 gaps) · proposed schema additions with reasoning · the module registration mechanism · a **13-phase plan** with an acceptance check, assigned documentation and named guardrails per phase · a catalogue of what the system could be used for. ### Five findings that contradict the brief this started from, and shape the plan - **There is no in-app channel.** Core has exactly one sink — the content-free push tickle (`utils/pushDispatch.js`). No inbox table, no read API, no list surface on web or Android. The in-app channel has to be built, not adapted. - **Email is already two-thirds of an engagement system, scoped to Teams.** Gmail OAuth2 is only the transport; `teamNotify.js` + `teamDigestWorker.js` + `team_notification_prefs` already do recipient selection, per-user opt-in, immediate-vs-digest, HMAC unsubscribe and RFC 8058 one-click. The plan generalises that pipeline and migrates Teams onto it rather than building a second one. - **The IDOC example's payload is not on the wire, and one field of it is not exactly knowable.** `house.decay` carries no `ownerName`, `nextStage` or collapse estimate. ServUO runs dynamic decay (`DynamicDecay.Enabled => Core.ML`) and draws each stage's duration **at random when that stage is entered**, so a collapse time is exact only once the house is at IDOC — earlier it can only be an envelope. Separately, the current mapping fires at `to == "IDOC"`, not at "greatly damaged", so the brief's own example does not fire today; fixing that needs **no** protocol change. - **Event-name collision handling already exists.** `modules/registries.js`'s `namespaced()` requires an `<owner>.` prefix on every stream and leg, `apply()` throws naming the current holder, and the loader already cross-checks N modules. §7.3 records the real forward-compat note instead (a rule whose module was uninstalled must go **dormant**, never error or auto-delete — the call `announce_job_legs` already made for an orphaned leg). - **`MODULE_API_VERSION` 1.6.0 is on `main` now**, so `MODULE_API.md` §1.1's "1.6.0 has only ever been on `edge`" argument is stale. The engagement additions take a real **1.7.0** (additions only ⇒ minor; `module-uo`'s `coreApi: "^1.3.0"` still resolves). ### Five scope decisions, settled by the org lead and recorded at the top of the doc 1. The in-app channel is in scope. 2. The Teams notification pipeline is generalised and migrated onto the new system. 3. The `house.decay` protocol enrichment is in scope, as a coordinated four-repo `PROTOCOL_VERSION` bump. 4. **Gmail OAuth2 is removed**, not retained as a transport. §1.2a is the deletion inventory. 5. The system ships with **seeded working templates plus an editor**, so a fresh deployment mails correctly before anyone opens the editor. ### The branching model, and two blocking findings (§6.0a) Every phase PR in every repo targets **`edge`**; `main` is touched exactly once, by the cutover (Phase 13). This workstream changes a wire protocol, a module API version and the mail path simultaneously, in different repos on different days — `main` must never hold a half-applied set. Checked 2026-08-28, both blocking before Phase 1: - **Every existing `edge` is stale** — 0 ahead of `main`, behind by: `docs` 16, `module-uo` 9, `installer` 7, `servuo-plugins` 7, `website` 5, `link` 3. Leftovers from previous cutovers, never refreshed. Fast-forwarding is lossless; skipping it means the cutover diff carries other workstreams' leftovers. (`docs` is now done, as part of this PR's retarget.) - **Three repos have no `edge` at all** — `android-app`, `runicgateway.com`, `Integration-kit`. Also recorded: `android-app/.gitea/workflows/pr-checks.yml` triggers only on PRs into `main`, so Phase 8 lands with **zero CI** and the cutover is its first real build — as happened to all nine M12 phase PRs. New open question Q8 asks whether to fix the trigger or accept that deliberately. ### Documentation is a phase deliverable, not an appendix (§6.0b) A phase-by-phase table assigning the specific docs each phase owes, in `docs/` and in every other repo, so nothing is deferred to a cleanup pass. §7.4 becomes the inventory that table draws from rather than a list of things to do at the end. ### Two new phases **Phase 12 — `runicgateway.com`.** Not optional polish; two of its own checks fail the build without it: - `scripts/checkFacts.mjs` fetches each fact's authority from the source repo's **`main`**, so `platform.json`'s `protocol` 4→5 (Phase 10) and `moduleApi` 1.6.0→1.7.0 (Phase 2) go red on their own — the protocol is asserted three times over (sidecar, `overlay.toml`, bundle). - The site currently documents **the opposite** of what Phase 1 ships: `administration/notifications-and-email.mdx` carries an `<Aside title="There is no SMTP option">` and *"Gmail over OAuth2 is the only supported delivery path today"*, plus `configuration.mdx:62`, `troubleshooting.mdx:101`, `system-architecture.mdx:117`. - `src/data/capabilities.mjs` already claims *"Web, push and email"* — the web channel does not exist until Phase 7. - `PLAY_DATA_SAFETY.md` and `/privacy` generate from **one inventory** that an engagement mailer materially changes: an address used beyond account function, a send log, a suppression list. Play's Data Safety form separates "app functionality" from "communications", so this is a store-review matter, not a doc nit. **The timing is the trap:** because `checkFacts` reads `main`, the site stays green through the entire `edge` period and breaks *the instant the cutover lands*. Phase 12 must be written before Phase 13 and merged in the same window. **Phase 13 — the cutover**, ordered rather than per-repo-independent: `docs` → `servuo-plugins` + `link` together (a protocol bump has three declaration sites; a `main` holding a v5 sidecar with a v4 overlay is a shard the installer refuses to pair) → `website` → `module-uo` → `Integration-kit` (its `core-ref.json` equality check is red until then, on purpose) → `android-app` → `runicgateway.com` last, because every fact it fetches must be true on `main` first. ### Two things worth a reviewer's attention - **G22 — the Gmail removal degrades silently.** At cutover `transport` backfills to `smtp` with no credentials, so every sink politely does nothing: contact form falls back to `mailto`, invites surface a copyable link, resets still answer a generic 200. Nothing breaks loudly, which means **email stops for the live deployment and nobody is told**. Phase 1 therefore owes an admin warning, a release note and operator guidance; Gmail-as-plain-SMTP with an app password is the documented migration path. - **G24 — an audience ceiling per trigger, and it is a security gap.** `shardStreams.js` already filters sensitive kinds off the public push path (`PUBLIC_KINDS`, "defense in depth"). The engine has no equivalent, so a rule could give `uo.cheat.detected` or `uo.audit.*` a public audience — a second door through the module system's boundary. A trigger must declare the widest audience it may ever have, and a rule must not be able to widen it. Placed in Phase 2's declaration, not Phase 4. Also adds one index row to the repo `README.md`. ## How it was tested Documentation only — nothing to build or run. Every claim was verified against the working trees and the live repos rather than recalled: - File/line references checked in `website/server/src`, `module-uo/server`, `android-app/app/src/main`, `runicgateway.com/src`. - `MODULE_API_VERSION` on `main` confirmed with `git show main:server/src/modules/version.js` (1.6.0). - The `house.decay` field set read from `servuo-plugins/.../BridgeSweeps.cs:199` and `link/INTEGRATION.md:207`; decay semantics from the local ServUO tree (`Scripts/Multis/BaseHouse.cs`, `Scripts/Multis/DynamicDecay.cs`). - Protocol declaration sites confirmed at `link/sidecar/src/main.rs:55` and `servuo-plugins/overlay.toml:27` (both `4`). - Every shard event kind in Part 8 taken from `link/INTEGRATION.md`, not invented. - Branch divergence measured per repo with `git rev-list --left-right --count origin/main...origin/edge`. - `runicgateway.com`'s fact authorities read out of `scripts/checkFacts.mjs` (each `raw(repo, path, 'main')` call), and the stale Gmail/SMTP prose located by grep across `src/`. Eight open questions are recorded in §7.1, each tagged with the phase it must be answered before. None block Phase 1 or Phase 2. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. *(Docs-only; no build or test surface.)* - [x] I have added or updated tests/docs where it makes sense. *(This PR is the doc; §6.0b assigns every follow-on doc edit to the phase that causes it.)* - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) This project **requires disclosure of AI tool usage**. Please pick one: - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude changed target branch from main to edge 2026-08-28 23:54:02 +00:00
wtclaude added 2 commits 2026-08-28 23:54:02 +00:00
Scoping investigation for an in-house, game-agnostic email and engagement
system: modules declare domain events and their data contract, core owns the
rules, preferences, templates and delivery.

Records the current-state map (notifications, email, the module contract and
the job/scheduling infrastructure), a gap list, the proposed schema additions,
the module registration mechanism, an eleven-phase plan with an acceptance
check per phase, and a catalogue of what the system could be used for.

Five findings contradict the brief this started from and shape the plan:

- There is no in-app channel. Core has one sink, the content-free push tickle;
  the in-app inbox has to be built, not adapted.
- Email is already two-thirds of an engagement system, scoped to Teams. The
  Teams pipeline is generalised and migrated onto the new one, not duplicated.
- The IDOC example's payload is not on the wire, and estimated_collapse is not
  exactly knowable in advance: ServUO draws each decay stage's duration at
  random when the stage is entered, so it is exact only at IDOC.
- Event-name collision handling already exists (registries.js namespaced() +
  apply()), so the brief's forward-compat note is already satisfied.
- MODULE_API_VERSION 1.6.0 is on main now, so the engagement additions take a
  real 1.7.0 rather than joining 1.6.0 in place.

Five scope decisions settled by the org lead are recorded at the top: the
in-app channel is in scope, the Teams pipeline is migrated, the house.decay
protocol enrichment is in scope, Gmail OAuth2 is removed rather than retained
as a transport, and the system ships with seeded templates plus an editor.

No code. Nothing is implemented until the org lead approves the phase.

Co-Authored-By: Claude <noreply@anthropic.com>
Three additions the org lead called for, all in ENGAGEMENT.md.

The branching model (new §6.0a). Every phase PR in every repo targets `edge`;
`main` is touched exactly once, by the cutover. Two blocking findings checked
on 2026-08-28: every existing `edge` is stale (0 ahead of `main`, behind by 16
docs / 9 module-uo / 7 installer / 7 servuo-plugins / 5 website / 3 link) and
must be fast-forwarded before the first phase PR, and three repos have no
`edge` at all — android-app, runicgateway.com and Integration-kit. Also records
that android-app's pr-checks.yml triggers only on PRs into `main`, so Phase 8
lands with no CI and the cutover is its first real build, as happened to all
nine M12 phase PRs.

Documentation as a phase deliverable (new §6.0b). A phase-by-phase table
assigning the specific docs each phase owes, in `docs/` and in every other
repo, so nothing is deferred to a cleanup pass. §7.4 becomes the inventory
that table draws from rather than a list of things to do at the end.

Two new phases. Phase 12 is runicgateway.com, which is not optional polish:
scripts/checkFacts.mjs fetches each fact's authority from the source repo's
`main`, so `protocol` 4 to 5 and `moduleApi` 1.6.0 to 1.7.0 fail its build on
their own. The site also currently documents the opposite of what Phase 1
ships — notifications-and-email.mdx carries a "There is no SMTP option" aside —
its capabilities list claims a web notification channel that will not exist
until Phase 7, and PLAY_DATA_SAFETY.md and /privacy generate from one inventory
that an engagement mailer materially changes. Because checkFacts reads `main`,
the site stays green through the whole `edge` period and breaks at the cutover,
so Phase 12 must be written before Phase 13 and merged in the same window.

Phase 13 is the cutover itself, ordered rather than per-repo-independent: docs,
then servuo-plugins and link together (a protocol bump has three declaration
sites and a `main` holding a v5 sidecar with a v4 overlay cannot pair), then
website, module-uo, Integration-kit, android-app, and runicgateway.com last
because every fact it fetches has to be true on `main` first.

Adds an eighth open question (fix the Android CI trigger, or accept the cutover
as its first build) and a Phase -1 to the sequencing diagram for the edge
fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 2a122445c9 into edge 2026-08-29 00:07:09 +00:00
whitlocktech deleted branch docs/engagement-plan 2026-08-29 00:07:09 +00:00
Sign in to join this conversation.
No description provided.