feat(engagement): the admin ceiling and core's news.post emitter (Phase 11a)
#177
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/engagement-uo-triggers"
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?
Core's half of ENGAGEMENT.md Phase 11a — the two of the six decisions that land here rather than in the module. Pairs with Module-uo#22 (the 24 trigger declarations) and docs#194 (the decisions themselves).
Decision 1 — a seventh ceiling,
admin, as a child ofstaffPhase 11's operator-facing triggers (
uo.audit.staff_action,uo.economy.milestone,uo.world.saved) are described as admin-audience everywhere they appear, and the narrowest value the lattice had wasstaff— whichceilings.jsdefines as admin, editor AND moderator. Ceilinging them there would have let an operator save a rule that mails the staff audit digest to every moderator in it.adminis the only genuine refinement in the whole tree — every admin is staff, which is exactly the containment every other pair of branches lacks — so it is a child rather than a seventh leaf, andpermits/meet/meetAllneeded no change beyond the newPARENTentry.permits('staff','admin')is now the single true pair belowauthenticated;permits('admin','staff')is false, as are both directions againstowner,membersandsubscribers.MODULE_API_VERSION→ 1.8.0 on both halves. Additive: every declaration valid under 1.7.0 is valid now, no stored value changes, and nothing anywhere stores anadminaudience yet — which is what made this cheap in this window and expensive after cutover.The one non-obvious consequence, and why
ROLE_CEILINGSis a tablenotificationChannelPrefs'visibleToaskeditem.ceiling !== 'staff'. That spelling was correct whilestaffwas the only role-gated value — and the dayadminarrived it would have silently published every admin-ceilinged id (the staff audit digest, the economy thresholds) by name, to every player's preferences screen. Exactly the disclosure the ceiling exists to prevent, introduced by adding the ceiling.It now reads a table via
ceilings.reachableBy, so a role-gated ceiling added without an entry there fails closed rather than leaking. An editor is the viewer that tells the two rules apart — insidestaff, outsideadmin— and the new tests use one:admin-ceilinged trigger is hidden from a player and from an editor, shown to an admin, and an editor who knows the id still cannot store a preference for it (it is a gate, not a display rule)staff-ceilinged trigger is still offered to an editor, so the generalisation did not quietly hide more than it shouldDecision 5 — §7.1 Q9:
news.postgets an emitter, and it replaces the ticklenews.posthas been a declared payload contract with no caller since Phase 2, so a rule naming it could never fire — on a real deployment the only mail or inbox item a rule could produce came from Teams.server/src/utils/newsNotify.jsis the caller;announceIfNewlyPublishednow calls it instead ofpushDispatch.publish, gated on the sameenqueueIfNeededjob id (the single "newly published news" transition signal, not re-derived — an edit or a re-publish still must not re-fire).The other two things a publish fires are untouched, and the header comment says why: the announce legs are a one-shot delivery to a channel of the deployment with retry, and the post hooks are idempotent state mirroring that also runs on delete. Neither is a per-person notification. This adds a third distinction of the same kind rather than replacing either.
The seed needed its own one-shot key — the trap worth recording
engagement_team_rules_seededis already stamped on every deployment that has booted since Phase 6, and the guard reads its presence. So appending news toRULESwould have seeded it on fresh installs only — and on exactly the upgrades that lose their raw news push, never. They would have lost the capability with no rule to switch on and no way to tell why.One key per seed group is now the rule.
seedGroup(key, rules, note)is the shared implementation;seedCoreRules()is what boot calls. The news rule carries all three channels (email,inapp,push) rather than the Team rules'emailalone — push is on it because push is what the raw tickle did, and leaving it off would mean an operator who enabled the rule to restore news push got mail instead.One defect found on the way
news.post'spostUrlexample named/news/five-on-friday-yew-invasion— a pathApp.jsxdoes not mount. It was invisible while the trigger had no caller. An example is what the template editor previews and test-sends with (§4.3 property 3), so a wrong one is a preview that looks right and a mail that is not. It is now/site/news, the list, which is whatannounceJobs.logic.jshas always linked from the Discord and town-crier announcements for the same reason.engagement-triggers.jsonregenerated.Verification
npm run check:modulesclean — core still names no module identifier, which is a materially stronger claim now that the module it is checked against registers 24 UO-named triggersengagement:manifest --checkgreen after regenerationAI disclosure
Co-Authored-By: Claudetrailer.🤖 Generated with Claude Code