Files
website/server/engagement-triggers.json
wtclaude 7d3d6d5abd
Some checks failed
PR Checks / client-build (pull_request) Successful in 45s
PR Checks / server-tests (pull_request) Failing after 5m47s
PR Checks / bot-tests (pull_request) Successful in 8m27s
feat(events): the integrations — lifecycle triggers, participants, results (Phase 10)
`EVENTS_PLAN.md` Phase 10. Core registers its own `event.` triggers, records who
took part, publishes a results table, and announces a post through the legs the
news pipeline already uses. Events owns none of the delivery: a run says what
happened and an operator's rule decides who is told, so email, the in-app inbox,
push tickles, Discord and the town crier all arrive without anything in
`events/` growing a second delivery path.

**No route was added and nothing moved.** The whole surface is two more derived
fields on a run — `participants` and `resultsPublishedAt` — and a zero-line
`routes.manifest.json` diff proves it.

Seven triggers: six at ceiling `authenticated` / audience `subscribers`, exactly
where `news.post` sits, and `run.failed` at `admin` on both halves. Every one
keys its cooldown on the RUN. Two rules seeded, both off, under a third one-shot
key so a deployment that has already stamped the Team and news keys still gets
them.

**The phase's own defect was a promise nothing kept.** `EVENTS.md` §I says a
rehearsal runs for real "with announcements ceilinged to `staff`" — but a
ceiling is declared on the TRIGGER, and a rehearsal fires the same trigger as
the real thing, so the moment this phase gave a run something to announce,
rehearsing a published event would have mailed every subscriber. The emit
envelope now takes an optional `ceiling` and the send-time G24 gate applies
`meet(declared, emitted)`. It only narrows; two incomparable ceilings refuse
every rule rather than resolving to either.

`MODULE_API_VERSION` stays 1.10.0, amended in place — `main` declares 1.9.0, so
1.10.0 has not shipped and the org lead's 2026-09-03 rule applies for the third
time.

Three defects the live walk found, none visible to a unit test:

1. **A channel that reported success while reaching nobody.** The seeded
   `run.started` rule named `push`, because §8.5 and the plan both do. Push
   delivery joins `notification_subscriptions`, only ever written for an id the
   preferences screen offered push for — and it offers push only for a
   registered STREAM. So the tickle went nowhere every time while
   `pushChannel.deliver` answered "tickle published". `event.run.started` is now
   a stream as well as a trigger; the other six are not.
2. **A trigger's `description` reaches a recipient.** It is the structural
   projection's `intro` fallback, so `run.failed`'s line ending "Staff-facing."
   put those words in an administrator's own inbox item.
3. **`affectedRows` cannot tell an insert from an unchanged upsert.** The
   connector sends `CLIENT_FOUND_ROWS`, so a "was this new" flag would have
   counted every idempotent retried collect as a fresh participant.

And one caught before it shipped: ranking with a session variable is wrong here,
because `query()` takes a pool connection per call — the variable would be set
on one connection and read on another. A window function needs no session state.

## Verification

- `npm test --prefix server` — **1981 pass, 1 fail**, and that one
  (`botScore.test.js`) passes standalone at 18/18: a file-level flake under
  parallel load. Run with an empty `MODULES_DIR`, as CI does.
- `npm test --prefix client` — 362 pass, 0 fail. `npm run build` green.
- Zero-line `routes.manifest.json` / `routes.guards.json` diff.
- A live walk on a real rig: MariaDB, the site with no module, mailpit. The mail
  arrived, headed with the event's title and its start time in the shard's own
  zone; the rehearsal fired the same trigger and produced zero outbox rows where
  the real run produced three; `run.failed` reached the administrator's inbox
  and no player's; `core.announce.post` queued a second job without touching the
  news pipeline's back-pointer or `announced_at`; and `rankRun` and the upsert
  were run against real MariaDB 11.

## One thing for a reviewer, out of scope and not fixed

**Every `#swagger.description` in this repo is truncated in the generated spec.**
swagger-autogen does not honour a backslash-escaped apostrophe, so a description
is cut at the first `\'` — 175 of the 177 in `server/src/router/**`. It is
pre-existing and repo-wide. Only the one annotation this phase edits is fixed
here (a typographic apostrophe), because otherwise this phase's own addition to
it would be dead text. The rest wants its own change.

- [x] AI-assisted: Claude Code (Opus 5).

Docs: RunicGateway/docs#TBD.

Co-Authored-By: Claude <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-04 13:06:46 -05:00

548 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"_comment": "Generated event-trigger inventory - the authoritative freeze of CORE's engagement contract (docs/website/ENGAGEMENT.md 4.3). Regenerate with `npm run engagement:manifest` in website/server. A renamed variable, a changed type or a widened ceiling breaks stored templates and rules, so the diff here is the review signal. A module ships its own copy in its bundle; this file never contains one.",
"moduleApiVersion": "1.10.0",
"triggers": [
{
"id": "event.phase.changed",
"owner": "core",
"label": "Event — a new phase",
"description": "An event that is under way has moved on to its next stage.",
"kind": "event",
"subjectKey": "runId",
"audience": "subscribers",
"ceiling": "authenticated",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
},
{
"name": "phase",
"type": "string",
"required": true,
"example": "assault",
"description": "The phase key just entered, as authored in the spec."
},
{
"name": "phaseLabel",
"type": "string",
"required": false,
"example": "The assault",
"description": "The phase label, when the spec gave it one. Falls back to the key."
},
{
"name": "phaseIndex",
"type": "int",
"required": true,
"example": 2,
"description": "Which phase this is, counting from 1."
},
{
"name": "phaseCount",
"type": "int",
"required": true,
"example": 4,
"description": "How many phases the pinned version has in total."
}
]
},
{
"id": "event.run.cancelled",
"owner": "core",
"label": "Event — cancelled",
"description": "A scheduled event was cancelled by a member of staff.",
"kind": "event",
"subjectKey": "runId",
"audience": "subscribers",
"ceiling": "authenticated",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
},
{
"name": "reason",
"type": "string",
"required": false,
"example": "The shard is down for an emergency patch.",
"description": "What the staff member gave as the reason, when they gave one."
}
]
},
{
"id": "event.run.completed",
"owner": "core",
"label": "Event — finished",
"description": "An event has finished.",
"kind": "event",
"subjectKey": "runId",
"audience": "subscribers",
"ceiling": "authenticated",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
},
{
"name": "summary",
"type": "string",
"required": false,
"example": "Orcish warbands are massing north of Yew.",
"description": "The event summary, as authored."
},
{
"name": "participantCount",
"type": "int",
"required": true,
"example": 47,
"description": "How many participants the run recorded. Zero when nothing collected any."
},
{
"name": "durationMinutes",
"type": "int",
"required": true,
"example": 95,
"description": "How long the run took, start to end, in whole minutes."
}
]
},
{
"id": "event.run.ending",
"owner": "core",
"label": "Event — winding down",
"description": "An event is drawing to a close.",
"kind": "event",
"subjectKey": "runId",
"audience": "subscribers",
"ceiling": "authenticated",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
}
]
},
{
"id": "event.run.failed",
"owner": "core",
"label": "Event — run failed",
"description": "An event stopped before it finished.",
"kind": "event",
"subjectKey": "runId",
"audience": "admin",
"ceiling": "admin",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
},
{
"name": "phase",
"type": "string",
"required": false,
"example": "assault",
"description": "The phase it failed in, when it had entered one."
},
{
"name": "error",
"type": "string",
"required": false,
"example": "sidecar responded 503",
"description": "The runs last error, verbatim from the run row."
},
{
"name": "runUrl",
"type": "url",
"required": true,
"example": "/admin/events/runs/3692",
"description": "Site-relative path to the run console."
}
]
},
{
"id": "event.run.scheduled",
"owner": "core",
"label": "Event — scheduled",
"description": "A new event has been added to the calendar.",
"kind": "event",
"subjectKey": "runId",
"audience": "subscribers",
"ceiling": "authenticated",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
},
{
"name": "summary",
"type": "string",
"required": false,
"example": "Orcish warbands are massing north of Yew.",
"description": "The event summary, as authored."
},
{
"name": "seriesName",
"type": "string",
"required": false,
"example": "The Yew Campaign",
"description": "The arc this event belongs to, when it belongs to one."
},
{
"name": "startsAt",
"type": "datetime",
"required": true,
"example": "2026-09-12T20:00:00.000Z",
"description": "When the occurrence is due to start, UTC."
},
{
"name": "timezone",
"type": "string",
"required": false,
"example": "America/New_York",
"description": "The shard-local zone the schedule was authored in."
},
{
"name": "startsAtLabel",
"type": "string",
"required": false,
"example": "Saturday 12 September at 8:00 pm (America/New_York)",
"description": "The start time written out in the shard-local zone, for a mail to read."
}
]
},
{
"id": "event.run.started",
"owner": "core",
"label": "Event — starting now",
"description": "A scheduled event has begun.",
"kind": "event",
"subjectKey": "runId",
"audience": "subscribers",
"ceiling": "authenticated",
"version": 1,
"variables": [
{
"name": "runId",
"type": "string",
"required": true,
"example": "3692",
"description": "The run this is about. Also the cooldown subject."
},
{
"name": "title",
"type": "string",
"required": true,
"example": "The Yew Invasion",
"description": "The event title."
},
{
"name": "summary",
"type": "string",
"required": false,
"example": "Orcish warbands are massing north of Yew.",
"description": "The event summary, as authored."
},
{
"name": "seriesName",
"type": "string",
"required": false,
"example": "The Yew Campaign",
"description": "The arc this event belongs to, when it belongs to one."
},
{
"name": "startsAt",
"type": "datetime",
"required": true,
"example": "2026-09-12T20:00:00.000Z",
"description": "When it actually started, UTC."
},
{
"name": "timezone",
"type": "string",
"required": false,
"example": "America/New_York",
"description": "The shard-local zone the schedule was authored in."
},
{
"name": "startsAtLabel",
"type": "string",
"required": false,
"example": "Saturday 12 September at 8:00 pm (America/New_York)",
"description": "The start time written out in the shard-local zone, for a mail to read."
}
]
},
{
"id": "news.post",
"owner": "core",
"label": "News post published",
"description": "A news / Five-on-Friday / newsletter post was published.",
"kind": "event",
"subjectKey": null,
"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."
},
{
"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."
}
]
},
{
"id": "team.announcement",
"owner": "core",
"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."
}
]
},
{
"id": "team.forum.post",
"owner": "core",
"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.leadership.changed",
"owner": "core",
"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.member.joined",
"owner": "core",
"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."
}
]
}
]
}