feat(events): the integrations — lifecycle triggers, participants, results (Phase 10) #192
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/events-phase-10"
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?
EVENTS_PLAN.mdPhase 10. Core registers its ownevent.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 inevents/growing a second delivery path.No route was added and nothing moved. The whole surface is two more derived fields on a run —
participantsandresultsPublishedAt— and a zero-lineroutes.manifest.jsondiff proves it.Seven triggers: six at ceiling
authenticated/ audiencesubscribers, exactly wherenews.postsits, andrun.failedatadminon both halves because a failure names the deployment's own broken machinery. Every one keys its cooldown on the RUN, so a weekly event is not throttled by last week's. Two rules seeded, both off, under a third one-shot key — so a deployment that has already stamped the Team and news keys still receives them.Six decisions (org lead, 2026-09-04), all as recommended
A narrowing
ceilingon the emit envelope · participants on the action's success envelope ·core.results.publishas an ordinary step · a nullableannounce_jobs.run_id· core's own seed mechanism rather than the module-facing one · §J's ceilings with two rules seeded rather than seven.A seventh followed from the walk:
event.run.startedbecomes a notification stream as well as a trigger.The phase's own defect was a promise nothing kept
EVENTS.md§I says a rehearsal runs for real "with announcements ceilinged tostaff". A ceiling is declared on the TRIGGER, and a rehearsal fires exactly 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 it.The emit envelope now takes an optional
ceilingand the send-time G24 gate appliesmeet(declared, emitted). It only narrows; two incomparable ceilings meet to null and every rule is refused rather than resolving to either, which issegments.js's own posture reused rather than re-argued. A rehearsal therefore exercises the announce steps, the rules and the log lines — everything except the delivery it must not make.MODULE_API_VERSIONstays 1.10.0, amended in place:maindeclares 1.9.0, so 1.10.0 has not shipped and the org lead's 2026-09-03 rule applies for the third time. (I called this a 1.11.0 bump when I asked; the rule says otherwise and the rule wins.)Four things the tree corrected about the plan
registerEngagementSeeds. That door is module-facing — template keys must be namespaced<owner>.— while core's own bodies live inengagement/templateSeeds.jsand its rules inengagement/coreRules.jsunder a per-group one-shot key.admin; §J says six are public. §J is right.ENGAGEMENT.md§8.5, not §8.6. §8.6 ismodule-uo's catalogue.phase.changedis the one most likely to be switched on by accident.The live walk, and the three defects it found
A real rig — MariaDB, the site with no module installed, mailpit as the relay — and a real two-phase event.
The mail arrived: "The Yew Invasion is starting", headed with the event's own title, its summary beneath it, and the start time reading "Friday 4 September at 1:54 pm (America/New_York)" rather than an ISO string. The series line was absent — the single-token block convention working, since this event belongs to no arc. A rehearsal of the same definition then logged
{"ceiling":"staff","because":"rehearsal"}and produced zero outbox rows where the real run produced three. A deliberately doomed run failed on a default-offcore.leaseandevent.run.failedreached the administrator's inbox and no player's.core.announce.postqueued a second job against an already-announced post without touchingposts.announce_job_id,findByPostIdorannounced_at. AndrankRun's window function and the participant upsert were run against real MariaDB 11: four participants ranked340 → 120.5 → 120.5 → -15with the tie broken deterministically and identically on a second call, and a re-report updated score anduser_idin place whilejoined_atandrank_atstayed put.None of the three was visible to a unit test.
run.startedrule namedpush, because §8.5's row and the plan's Ships line both do. Push delivery joinsnotification_subscriptions, which is only ever written for an id the preferences screen offered push for — andnotificationChannelPrefs.catalogoffers push only for a registered stream. So the tickle went to nobody, every time, whilepushChannel.deliverstill answeredok: true, 'tickle published'.event.run.startedis now a stream as well as a trigger; the other six are not, because push is the channel that says now and the rest are things a player reads when they next look. The rule this settles: every stream must be a trigger; a trigger need not be a stream.descriptionis read by a recipient, not only by an operator. It is the structural projection'sintrofallback, sorun.failed's line ending "Staff-facing." put those words in an administrator's own inbox item. All five jargon-y lines were rewritten as prose. Who a trigger is for is what its ceiling says.affectedRowscannot tell an insert from an unchanged upsert. The connector sendsCLIENT_FOUND_ROWS, under which anON DUPLICATE KEY UPDATEthat changes nothing answers 1 — the same as an insert. A{ inserted }flag would have counted every idempotent retried collect as a fresh participant. Third occurrence of this flag's class, after Engagement Phase 4a's cooldown.And one caught before it shipped: ranking with a session variable is wrong here, because
query()takes a pool connection per call —SET @rk := 0lands on one connection and theUPDATEreads another. A window function needs no session state at all.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 emptyMODULES_DIR, as CI does — withmodules/uoinstalled the two route-manifest tests fail by construction on this machine.npm test --prefix client— 362 pass, 0 fail.npm run buildgreen.routes.manifest.jsonandroutes.guards.jsondiffs.One thing for a reviewer, out of scope and deliberately not fixed
Every
#swagger.descriptionin 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 inserver/src/router/**, which is essentially every route description in the published OpenAPI document. 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.Docs: RunicGateway/docs#218.
🤖 Generated with Claude Code