// ── Core's own engagement triggers ───────────────────────────────────────── // // ENGAGEMENT.md §4.3 and Phase 2. The twin of config/coreStreams.js, and // deliberately the SAME FIVE IDS — that is the org lead's §7.2 decision, taken at // the start of this phase: **one namespace.** A trigger is not a second thing // standing next to a stream; it is a payload contract attached to an id that may // also carry a subscription toggle. `news.post` names one event, whether the // question being asked of it is "may I push this?" or "what may a template // interpolate?". // // What that buys, concretely: `notification_channel_prefs.stream_id` (§4.5) stays // single-keyed. Under two namespaces it would have needed a `kind` discriminator // in its primary key, and `news.post` would have named two different things // forever. // // What it costs is the rule enforced in registries.js: an id has ONE owner across // both facets, so a module cannot attach a payload contract to another module's // stream, and core cannot attach one to a module's. Core's five ids below are // already core's five streams, so all five are the same-owner upgrade case. // // **These declare; nothing here emits yet.** Phase 2 is the contract only — the // Team pipeline keeps its own hardcoded mail until Phase 6 migrates it onto the // engine, and this file is what it migrates ONTO. Registering the declarations a // phase early is the same decision registerCore() has always taken: a registry // whose first real exercise is a module is a registry that has already drifted. // // Every variable carries an `example`, and that is required rather than // decorative (§4.3 property 3). It is what lets the template editor preview and // test-send without a live game event, which is the reason template systems go // untested. const TRIGGERS = [ { id: 'news.post', label: 'News post published', description: 'A news / Five-on-Friday / newsletter post was published.', kind: 'event', // No subjectKey. The subject of a cooldown here is the USER, not the post — // "do not mail me about news more than once an hour" is the useful rule, and // keying it per post would make every cooldown a no-op. Compare the four // Team triggers below, where the Team genuinely is the subject. audience: 'subscribers', ceiling: 'authenticated', version: 1, variables: [ { name: 'title', type: 'string', required: true, example: 'Five on Friday — the Yew invasion', description: 'The post title.' }, { name: 'excerpt', type: 'string', required: false, example: 'Four new champion spawns, and the fate of the Yew moongate…', description: 'A plain-text summary, already stripped of markup.' }, { name: 'category', type: 'string', required: false, example: 'Five on Friday', description: 'The post category, when it has one.' }, // **`/site/news`, the LIST, and not a per-post path.** The example said // `/news/` when this was declared with no caller; Phase 11 gave it // one and the path turned out not to exist — `App.jsx` mounts `/site/news` // and nothing under it, which is why `announceJobs.logic.js` links the list // from the Discord and town-crier announcements too. An `example` is what // the template editor previews and test-sends with (§4.3 property 3), so an // example naming a 404 is a preview that looks right and a mail that is not. { name: 'postUrl', type: 'url', required: true, example: '/site/news', description: 'Site-relative path to the post. The news list today — the site has no per-post route.' }, ], }, // ── Teams (TEAMS.md Part 6) ───────────────────────────────────────────── // // All four ceiling at `members` and not one of them higher. Who may be told // about a Team event is the access resolver's answer and always has been // (coreStreams.js says the same thing about the push catalog); the ceiling is // that rule written where a RULE EDITOR has to obey it too. Without it an // operator could point a rule at `authenticated` and mail a private Team's // forum excerpt to the whole site. { id: 'team.member.joined', label: 'Team — new member', description: 'Someone joined a Team.', kind: 'event', subjectKey: 'teamName', audience: 'members', ceiling: 'members', version: 1, variables: [ { name: 'teamName', type: 'string', required: true, example: 'The Silver Anvil', description: 'The Team the event is about. Also the cooldown subject.' }, { name: 'memberName', type: 'string', required: true, example: 'Darrow', description: 'Display name of the member who joined.' }, { name: 'teamUrl', type: 'url', required: false, example: '/guilds/the-silver-anvil', description: 'Site-relative path to the Team page. Absent when no module supplies a pageUrlTemplate.' }, ], }, { id: 'team.leadership.changed', label: 'Team — leadership change', description: 'Leadership changed in a Team.', kind: 'event', subjectKey: 'teamName', audience: 'members', ceiling: 'members', version: 1, variables: [ { name: 'teamName', type: 'string', required: true, example: 'The Silver Anvil', description: 'The Team the event is about. Also the cooldown subject.' }, { name: 'leaderName', type: 'string', required: true, example: 'Marisol', description: 'Display name of the new leader.' }, { name: 'teamUrl', type: 'url', required: false, example: '/guilds/the-silver-anvil', description: 'Site-relative path to the Team page.' }, ], }, { id: 'team.forum.post', label: 'Team — new forum post', description: 'A new thread or reply in a Team forum.', kind: 'event', subjectKey: 'teamName', audience: 'members', ceiling: 'members', version: 1, variables: [ { name: 'teamName', type: 'string', required: true, example: 'The Silver Anvil', description: 'The Team the event is about. Also the cooldown subject.' }, { name: 'authorName', type: 'string', required: true, example: 'Darrow', description: 'Display name of the poster.' }, { name: 'threadTitle', type: 'string', required: true, example: 'Tuesday champ rotation', description: 'Title of the thread the post belongs to.' }, { name: 'excerpt', type: 'string', required: false, example: 'Moving the Tuesday run an hour later…', description: 'Plain-text excerpt of the post body, already stripped of markup.' }, { name: 'postUrl', type: 'url', required: false, example: '/guilds/the-silver-anvil/forum/412', description: 'Site-relative path to the post.' }, ], }, { id: 'team.announcement', label: 'Team — announcement', description: 'A leader posted an announcement in a Team.', kind: 'event', subjectKey: 'teamName', audience: 'members', ceiling: 'members', version: 1, variables: [ { name: 'teamName', type: 'string', required: true, example: 'The Silver Anvil', description: 'The Team the event is about. Also the cooldown subject.' }, { name: 'authorName', type: 'string', required: true, example: 'Marisol', description: 'Display name of the leader who posted.' }, { name: 'title', type: 'string', required: true, example: 'Siege practice moved to Sunday', description: 'The announcement title.' }, { name: 'excerpt', type: 'string', required: false, example: 'We are moving practice to Sunday 8pm…', description: 'Plain-text excerpt of the announcement body.' }, { name: 'postUrl', type: 'url', required: false, example: '/guilds/the-silver-anvil/forum/419', description: 'Site-relative path to the announcement.' }, ], }, ] module.exports = { TRIGGERS }