docs(website): add ENGAGEMENT.md, the engagement system design of record #176
Reference in New Issue
Block a user
No description provided.
Delete Branch "docs/engagement-plan"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
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.teamNotify.js+teamDigestWorker.js+team_notification_prefsalready 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.house.decaycarries noownerName,nextStageor 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 atto == "IDOC", not at "greatly damaged", so the brief's own example does not fire today; fixing that needs no protocol change.modules/registries.js'snamespaced()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 callannounce_job_legsalready made for an orphaned leg).MODULE_API_VERSION1.6.0 is onmainnow, soMODULE_API.md§1.1's "1.6.0 has only ever been onedge" argument is stale. The engagement additions take a real 1.7.0 (additions only ⇒ minor;module-uo'scoreApi: "^1.3.0"still resolves).Five scope decisions, settled by the org lead and recorded at the top of the doc
house.decayprotocol enrichment is in scope, as a coordinated four-repoPROTOCOL_VERSIONbump.The branching model, and two blocking findings (§6.0a)
Every phase PR in every repo targets
edge;mainis 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 —mainmust never hold a half-applied set.Checked 2026-08-28, both blocking before Phase 1:
edgeis stale — 0 ahead ofmain, behind by:docs16,module-uo9,installer7,servuo-plugins7,website5,link3. Leftovers from previous cutovers, never refreshed. Fast-forwarding is lossless; skipping it means the cutover diff carries other workstreams' leftovers. (docsis now done, as part of this PR's retarget.)edgeat all —android-app,runicgateway.com,Integration-kit.Also recorded:
android-app/.gitea/workflows/pr-checks.ymltriggers only on PRs intomain, 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.mjsfetches each fact's authority from the source repo'smain, soplatform.json'sprotocol4→5 (Phase 10) andmoduleApi1.6.0→1.7.0 (Phase 2) go red on their own — the protocol is asserted three times over (sidecar,overlay.toml, bundle).administration/notifications-and-email.mdxcarries an<Aside title="There is no SMTP option">and "Gmail over OAuth2 is the only supported delivery path today", plusconfiguration.mdx:62,troubleshooting.mdx:101,system-architecture.mdx:117.src/data/capabilities.mjsalready claims "Web, push and email" — the web channel does not exist until Phase 7.PLAY_DATA_SAFETY.mdand/privacygenerate 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
checkFactsreadsmain, the site stays green through the entireedgeperiod 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+linktogether (a protocol bump has three declaration sites; amainholding a v5 sidecar with a v4 overlay is a shard the installer refuses to pair) →website→module-uo→Integration-kit(itscore-ref.jsonequality check is red until then, on purpose) →android-app→runicgateway.comlast, because every fact it fetches must be true onmainfirst.Two things worth a reviewer's attention
transportbackfills tosmtpwith no credentials, so every sink politely does nothing: contact form falls back tomailto, 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.shardStreams.jsalready filters sensitive kinds off the public push path (PUBLIC_KINDS, "defense in depth"). The engine has no equivalent, so a rule could giveuo.cheat.detectedoruo.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:
website/server/src,module-uo/server,android-app/app/src/main,runicgateway.com/src.MODULE_API_VERSIONonmainconfirmed withgit show main:server/src/modules/version.js(1.6.0).house.decayfield set read fromservuo-plugins/.../BridgeSweeps.cs:199andlink/INTEGRATION.md:207; decay semantics from the local ServUO tree (Scripts/Multis/BaseHouse.cs,Scripts/Multis/DynamicDecay.cs).link/sidecar/src/main.rs:55andservuo-plugins/overlay.toml:27(both4).link/INTEGRATION.md, not invented.git rev-list --left-right --count origin/main...origin/edge.runicgateway.com's fact authorities read out ofscripts/checkFacts.mjs(eachraw(repo, path, 'main')call), and the stale Gmail/SMTP prose located by grep acrosssrc/.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
AI-assisted contributions (required)
This project requires disclosure of AI tool usage. Please pick one:
Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with aCo-Authored-By/Assisted-Bytrailer.License