docs(website): §7.1 Q9 — core's own news.post emitter #189

Merged
whitlocktech merged 1 commits from docs/engagement-news-emitter-note into edge 2026-08-31 07:30:56 +00:00
Showing only changes of commit 66257ebcb5 - Show all commits

View File

@@ -2764,7 +2764,10 @@ declared trigger that **nothing emits through the engine** — `coreTriggers.js`
admin publish path still fires a raw `pushDispatch.publish` beside the engine rather than through it. admin publish path still fires a raw `pushDispatch.publish` beside the engine rather than through it.
The consequence for this phase: on a real deployment the only in-app items a rule can produce today The consequence for this phase: on a real deployment the only in-app items a rule can produce today
come from the four Team triggers. Wiring the news emitter is a one-line `ctx.events.emit`-shaped change come from the four Team triggers. Wiring the news emitter is a one-line `ctx.events.emit`-shaped change
that belongs with whoever owns that decision, not smuggled into the channel's own phase. that belongs with whoever owns that decision, not smuggled into the channel's own phase. **Written up as
§7.1 Q9**, which sets out the three other things a publish already fires (an announce leg, a module's
post hook, the raw tickle), which of them the engine replaces and which it must not touch, and the
continuity question that has to be answered before anyone writes the line. Recommended home: Phase 11.
**Still later phases':** the app's inbox screen and the tickle → pull → inbox path (Phase 8), and the **Still later phases':** the app's inbox screen and the tickle → pull → inbox path (Phase 8), and the
suppression list, which this channel has no equivalent of — there is no address to suppress. suppression list, which this channel has no equivalent of — there is no address to suppress.
@@ -2827,10 +2830,19 @@ starts early and lands independently.
with Phase 6 if Phase 10 is still in flight — the trigger simply omits `nextStage`/`estimatedCollapse` with Phase 6 if Phase 10 is still in flight — the trigger simply omits `nextStage`/`estimatedCollapse`
until the v5 overlay is deployed, which the `required: false` declaration already permits. until the v5 overlay is deployed, which the `required: false` declaration already permits.
**Also core's own `news.post` emitter, which this phase's title understates** (§7.1 Q9). `news.post` is
declared with no caller, so a rule naming it can never fire; wiring it is one `ctx.events.emit`-shaped
call in `announceIfNewlyPublished`, gated on the same job-id transition signal the push already uses.
**The announce legs and the post hooks are untouched** — a module's town-crier leg is a delivery to a
channel of the deployment and its news-gump hook is state mirroring, neither of which is a per-person
notification. What the emit replaces is the raw `pushDispatch.publish` beside them, and Q9's continuity
question has to be answered before it does.
**Acceptance:** the five-rung shard visibility walk still shows no leak; a house transitioning to **Acceptance:** the five-rung shard visibility walk still shows no leak; a house transitioning to
`Greatly` on the live rig produces one email to the linked owner and nothing to anyone else; a second `Greatly` on the live rig produces one email to the linked owner and nothing to anyone else; a second
transition inside the cooldown produces nothing; a refresh back to `LikeNew` inside the delay window transition inside the cooldown produces nothing; a refresh back to `LikeNew` inside the delay window
cancels the pending mail. cancels the pending mail; **a news post published on the rig reaches a rule, and the town-crier leg and
every registered post hook still fire exactly as they did.**
**Guardrails:** `check:modules` proves core gained no UO identifier across every phase to this point. **Guardrails:** `check:modules` proves core gained no UO identifier across every phase to this point.
--- ---
@@ -2963,7 +2975,7 @@ day it ships.
## Part 7 — Open questions and forward-compat notes ## Part 7 — Open questions and forward-compat notes
### 7.1 Questions for the org lead — seven answered, one still open ### 7.1 Questions for the org lead — seven answered, two still open
1. ✅ **ANSWERED — may unverified addresses receive engagement mail?** *"Emails need to be unique and 1. ✅ **ANSWERED — may unverified addresses receive engagement mail?** *"Emails need to be unique and
verification blocking sending is an admin setting."* Combined with the opt-in answer, this settles verification blocking sending is an admin setting."* Combined with the opt-in answer, this settles
@@ -3026,6 +3038,52 @@ day it ships.
nine M12 phase PRs. Fix the trigger as Phase 8's first commit, or accept it deliberately? nine M12 phase PRs. Fix the trigger as Phase 8's first commit, or accept it deliberately?
Recommendation: fix it. It is a two-line workflow change and the alternative is finding out about a Recommendation: fix it. It is a two-line workflow change and the alternative is finding out about a
Kotlin compile error during the cutover window. Kotlin compile error during the cutover window.
9. **Core's own `news.post` emitter — and the three other things a publish already fires.**
`config/coreTriggers.js` declares core's five triggers and says in as many words that **nothing here
emits yet**; Phase 6 migrated only the four `team.*` ones onto the engine. So `news.post` is a
declared payload contract with **no caller**, and on a real deployment the only in-app or email items
a rule can produce today come from Teams. Phase 7 found this and deliberately did not wire it, on the
grounds that a channel's own phase is not the place to give another phase's trigger an emitter.
**The call itself is one line. The care is entirely in what it must not disturb.**
`admin.controller.js`'s `announceIfNewlyPublished` already fans one publish four ways, and they are
different in kind — three of them are *not* the engagement engine's business:
| What fires on a publish | Whose | What kind of thing it is | The engine's? |
| --- | --- | --- | --- |
| `announceJobs.enqueueIfNeeded` → `announce_job_legs` | core, with legs registered by modules — `module-uo` owns `towncrier` | a one-shot **delivery to a channel of the deployment**, with retry and classification | **No** |
| `registries.dispatchPostHook('onSaved')` | modules, via `registerPostHook` (API 1.1.0) | idempotent **state mirroring** — it also runs on delete, and refreshes silently on an edit | **No** |
| `pushDispatch.publish('news.post', { ref })` | core | a **per-person notification**, to whoever subscribed | **Yes — this is the one that should become an emit** |
| *(missing)* `engagementEmit.emit('core', 'news.post', …)` | core | rules → email / in-app / push | **Yes** |
`registries.js` already states the first two apart and why they were not folded together ("a leg is a
one-shot DELIVERY with retry and classification; a post hook maintains idempotent STATE, has to run on
delete as well as save, and refreshes silently on an edit"). **Adding the engine makes a third
distinction of the same kind, not a replacement for either.** A module's town-crier leg and a module's
news gump must keep firing exactly as they do; what changes is only that the raw tickle stops being
the one person-facing sink and becomes one channel of a rule.
**What a module gets out of this, stated so nobody widens it by accident.** A module already has two
doors onto a news publish — the announce leg and the post hook — and it keeps both. What it does *not*
get is the ability to fire `news.post` itself: the id's owner is core, `ctx.events.emit` binds the
owner at the call and never reads it from the arguments, and §7.2's one namespace means an id has
exactly one owner across both facets. A module that wants a person-facing notification of its own
declares its own trigger through `registerEventTriggers`. That is the whole of "modules can use it".
**Three things to settle before anyone writes the line:**
- **Continuity, and it is the same shape as G22 and Phase 6's decision 3.** Today publishing news
tickles every `news.post` subscriber directly. If the emit *replaces* that call, push stops the
moment this lands and stays stopped until an operator enables a rule — silently, because `enabled`
defaults to `0`. Either core seeds a `news.post` rule (and then: enabled, against the standing
default, or disabled with a banner as the Team rules got?), or the raw tickle stays beside the emit
for one release and is removed once a rule is known to exist.
- **The transition signal must be reused, not re-derived.** `enqueueIfNeeded` returning a truthy job
id is the single "newly published news" test, and the push call already piggybacks on it
deliberately so an edit or a re-publish does not re-fire. The emit must gate on the same value; a
second reading of the transition is a second chance to disagree with the first.
- **Which phase owns it.** Recommendation: **Phase 11**, which already ships "the first real rule" and
is where a declared trigger first gets a caller. It is core work rather than `module-uo`'s, so that
phase's title understates it — say so there rather than inventing a phase for one call site.
### 7.2 One namespace, or two? ### 7.2 One namespace, or two?