docs(website): record the org lead's answers to Q1/Q3/Q5/Q7, and add Phase 1b #177
Reference in New Issue
Block a user
No description provided.
Delete Branch "docs/engagement-decisions"
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
Four of
ENGAGEMENT.md§7.1's eight open questions were answered by the org lead on 2026-08-28. Two of the answers were larger than the questions — Q1 carries a whole new phase, and Q7 adds a third addition to the module contract. This PR records all four, and the findings that had to be verified before they could be written down.Docs only. Phase -1 (the
edgeprep §6.0a made blocking) was also executed while this was written, and is recorded as complete.The four answers
smtp.gmail.com:587) explicitly as the migration path off OAuth2.enableddefaults to0and every rule carries a hard per-hour send ceiling. Addsmax_sends_per_hourtoengagement_rules. The ceiling is the thing that makes "data" safe enough to pick over "code".users.emailbecomes UNIQUE; the verification gate is an admin setting, on for fresh installs, off for upgrades. See below.Q2, Q4, Q6 and Q8 remain open, blocking Phases 4, 5b, 2 and 8.
Q7 → §5.1a, audiences
The correction was precise and worth quoting: "there is no campaign in the normal sense of email marketing, but admins can create all sorts of trigger conditions", and separately "lists can be built if they are powered by game data — say team X members or governors — thru the uo module; same surface will be exposed to all modules."
So a third contract addition,
api.registerAudiences, on the same registration discipline as the triggers. Two rules carry the weight:engagement_audience_segments.ceilingis therefore derived at save time, never operator-typed.Q1 → §0.6 and a new Phase 1b
"Emails need to be unique" reads like a schema tweak. It is not —
users.emailisVARCHAR(255) NULLwith no index andschema.sql:24says so on purpose. Four findings, each read out of the tree:ALTERwould stop the site. Upgrades ride the idempotent block atschema.sql:1409+, run byensureSchema()on every boot (server.js:66).ADD UNIQUE INDEXagainst a table holding duplicates throws, and the site does not start. De-duplication must run first, in the same release.isDuplicateUsername()never inspects which index collided —users.model.js:24isER_DUP_ENTRY || 1062and nothing more. Unfixed,auth.controller.js:137answers "That username is already taken." for a duplicate email, andsso.controller.js:197retries usernames for an email conflict, which can never clear, so it burnsPROVISION_MAX_TRIESand fails opaquely. This fix must be in the tree before the index is, not after.provisionSsoPlayerdoes exactly that whenplayer_registration ∈ {sso, both}, writingprofile.emailin withemailVerified: Boolean(profile.email)— verified because an address was present, not because the IdP asserted it. That is also why de-dupe is oldest-wins rather than verified-wins: the verified flag is too weak a signal to arbitrate on.Plus the error-surface consequence: a unique constraint needs a user-facing failure, and the obvious wording makes account existence queryable — against a posture the codebase holds deliberately (
passwordReset.controller.jsanswers a generic 200 "to avoid account enumeration"). Settled: generic message, real reason logged not returned, rate limit kept, and not fed to the bot scorer, so an honest typo on a taken address cannot push a legitimate user toward an IP ban.Phase 1b lands between 1 and 2 and alone. Two knock-ons: Phase 9 no longer blocks Phase 11 (the verification mechanism moved forward, Phase 9 keeps bounces and suppression), and Phase 2 now follows 1b — an audience resolves to users, and the identity those users are mailed at should be unique before anything resolves over it.
A wrong turn, kept in the doc on purpose
The first commit "corrected" §6.0a to say
android-appalready had anedge. It did not.git fetch origindoes not prune, so arefs/remotes/origin/edgeleft from the branch deleted after the M12 cutover still resolved —rev-parsesucceeded andrev-list --left-right --countreturned "1 behind", which reads exactly like a stale-but-present branch. The push reported[new branch]andgit ls-remote --heads origin edgeconfirmed it. The second commit reverts the claim and records the trap: a remote-tracking ref is a cache, not an answer.How it was tested
Documentation only. Every claim verified against the working trees rather than recalled —
schema.sql(users DDL, the ALTER block),users.model.js,auth.controller.js,sso.controller.js,player/account.router.js,server.js. Remote branch existence checked withls-remote, not remote-tracking refs.Phase -1 executed alongside: five fast-forwards (
module-uo9 behind,installer7,servuo-plugins7,website5,link3) and three branches cut frommain(android-app,runicgateway.com,Integration-kit). Each fast-forward verifiededge == origin/mainandorigin/edgean ancestor of the result before pushing.Checklist
AI-assisted contributions (required)
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