Three defects the acceptance walk found in shipped code. **The public calendar showed neither what is live nor what is recent.** §I says `GET /public/events` is "the calendar: upcoming, **live** and **recent**". Built, it was upcoming only: `listInWindow` filtered on `scheduled_for >= from` alone and the shipped page asks for no window at all, so it took the default of now → +31d. A run that began five minutes ago and has three hours to go was absent; so was one that ended an hour ago. The site contradicted itself — `live: true` on `/site/events/<slug>` while `/site/events` served `entries: []`. A run is an interval, not an instant. `listInWindow` now matches a run whose occupied interval OVERLAPS the window, which fixes the admin calendar's identical hole (a run that started last Sunday and is still going was missing from "this week"), and the public default reaches `DEFAULT_RECENT_DAYS` back so "recent" has somewhere to live. Forecasts are still computed from `now`, never from the tail: a projection into the past would advertise an occurrence that did not happen. **A resource left `reverting` by a crash was never reclaimed.** `claimRevert`'s comment said `reverting` is not claimable "exactly as a step with a live claim is" — but a step's claim carries `claim_expires_at` and is reclaimed when the lease lapses, and a resource in `reverting` had no expiry and nothing released it. A process killed mid-teardown stranded the row for good: the sweep skipped it every 15s for ever, `cleanup_status` never left `pending`, and `POST …/cleanup` — the recourse §I names — answered 200 and did nothing, because it claims through the same function. On the rig it stranded a lease, which then BLOCKED the next run of the same event from taking that value until the shard's own deadline lapsed. The stale test is `updated_at`, which for a `reverting` row is exactly when the claim was taken, so no column is added. `updated_at` is re-stamped explicitly and that is load-bearing rather than tidy: this connector sends `CLIENT_FOUND_ROWS`, so without the write a second claimer would still match the row. `revert_attempts` is untouched — a stale claim is a process that died, not an attempt that failed. **Three facts every event announcement computed and none could use.** `announce.js` `baseFor()` puts `summary`, `seriesName` and `timezone` on all seven `event.*` payloads, but four triggers declared none of them and a fifth declared one, so `validatePayload` dropped them, they were absent from the variable list an author picks from, and every emit logged `emit carried undeclared variables` at DEBUG. They are now one shared `EVENT_AMBIENT` declaration spread into all seven, with the per-trigger copies removed so the seven cannot drift. Verified against a real ServUO + sidecar + website rig: the public page now shows a live run as "Happening now" beside recent finished ones (it showed nothing at all before), and a lease stranded by a real mid-teardown crash was reclaimed within one sweep, taking `cleanup_status` from `pending` to `complete`. The three `claimRevert` tests live in `eventRunnerSql.test.js` against a real MariaDB, because every part of the answer is the server's — `NOW() - INTERVAL`, `ON UPDATE`, and above all what `affectedRows` counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
688 lines
21 KiB
JSON
688 lines
21 KiB
JSON
{
|
||
"_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": 2,
|
||
"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’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"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."
|
||
},
|
||
{
|
||
"name": "eventUrl",
|
||
"type": "url",
|
||
"required": false,
|
||
"example": "/site/events/the-yew-invasion?run=3692",
|
||
"description": "The public page for this occurrence."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"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": 2,
|
||
"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’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"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."
|
||
},
|
||
{
|
||
"name": "eventUrl",
|
||
"type": "url",
|
||
"required": false,
|
||
"example": "/site/events/the-yew-invasion?run=3692",
|
||
"description": "The public page for this occurrence."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "event.run.completed",
|
||
"owner": "core",
|
||
"label": "Event — finished",
|
||
"description": "An event has finished.",
|
||
"kind": "event",
|
||
"subjectKey": "runId",
|
||
"audience": "subscribers",
|
||
"ceiling": "authenticated",
|
||
"version": 2,
|
||
"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’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"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."
|
||
},
|
||
{
|
||
"name": "eventUrl",
|
||
"type": "url",
|
||
"required": false,
|
||
"example": "/site/events/the-yew-invasion?run=3692",
|
||
"description": "The public page for this occurrence."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"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": 2,
|
||
"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’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"name": "eventUrl",
|
||
"type": "url",
|
||
"required": false,
|
||
"example": "/site/events/the-yew-invasion?run=3692",
|
||
"description": "The public page for this occurrence."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"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": "summary",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "Orcish warbands are massing north of Yew.",
|
||
"description": "The event’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"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 run’s 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": 2,
|
||
"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’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"name": "startsAt",
|
||
"type": "datetime",
|
||
"required": true,
|
||
"example": "2026-09-12T20:00:00.000Z",
|
||
"description": "When the occurrence is due to start, UTC."
|
||
},
|
||
{
|
||
"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."
|
||
},
|
||
{
|
||
"name": "eventUrl",
|
||
"type": "url",
|
||
"required": false,
|
||
"example": "/site/events/the-yew-invasion?run=3692",
|
||
"description": "The public page for this occurrence."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"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": 2,
|
||
"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’s one-line summary, when it has one."
|
||
},
|
||
{
|
||
"name": "seriesName",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "The Yew Campaign",
|
||
"description": "The arc this event belongs to, when it belongs to one."
|
||
},
|
||
{
|
||
"name": "timezone",
|
||
"type": "string",
|
||
"required": false,
|
||
"example": "America/New_York",
|
||
"description": "The zone the run was computed in — what a time in the body should be read as."
|
||
},
|
||
{
|
||
"name": "startsAt",
|
||
"type": "datetime",
|
||
"required": true,
|
||
"example": "2026-09-12T20:00:00.000Z",
|
||
"description": "When it actually started, UTC."
|
||
},
|
||
{
|
||
"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."
|
||
},
|
||
{
|
||
"name": "eventUrl",
|
||
"type": "url",
|
||
"required": false,
|
||
"example": "/site/events/the-yew-invasion?run=3692",
|
||
"description": "The public page for this occurrence."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"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."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|