5.7.11 Gate the watcher TRANSITION email by the `transitioned · email` preference — `watcherNotificationsService` never consults the resolver
Estimate: 22m · Depends on: 5.7.6, 5.4.5
Why this exists (the seam nobody owned — notes.html mistake #40). 5.7.6 built the channel-preference resolver and wired the EMAIL gate into the 5.1.6 mention job (mentionNotificationsService.filterChannelEnabled) only. The 5.4.5 watcher email fan-out (watcherNotificationsService.fanOut) shipped earlier and was never wired — it enqueues an email.send for every viewable watcher with NO isChannelEnabled / filterChannelEnabled call. So a watcher who turns transitioned · email OFF still receives the transition email: without this fix the matrix toggle (5.7.12) would be DECORATIVE — the "worse than disabled" defect the bug card warns about.
Fix. In watcherNotificationsService.fanOut, on the TRANSITION branch, gate each watcher page through notificationPreferencesService.filterChannelEnabled(pageUserIds, 'transitioned', 'email') BEFORE enqueuing email.send — applied per page so the gate rides the existing bounded cursor walk (finding #57), one batch query per page. Keep the existing exclusions (actor; on comments, mentioned users) and the send-time view re-check unchanged. The event still fires once; the job just asks the resolver before dispatching (the one-emit-path invariant — same shape as the 5.7.6 mention-job gate). An unset preference resolves to the documented default (ON), so untouched watchers are unaffected.
Scope note (out of scope — log as a finding, do NOT absorb). The watcher COMMENT email (the kind: 'comment' branch) is ALSO ungated, but it has no clean matrix row: the matrix's commented row is the involved/mention path, not the watching path. A "watching · commented" preference is a separate design question (a new matrix row) — note it in the PR body as a finding; this subtask gates the TRANSITION branch by transitioned only.
Acceptance criteria
- With
transitioned · emailset OFF, awork-item/transitionedevent the watcher would otherwise receive does NOT enqueueemail.sendfor that watcher — verified by an integration test against the realwatcherNotificationsService.fanOut(mirror the existing watcher fan-out tests). - An unset / ON preference still delivers (default-on preserved); other recipients on the same event are unaffected; the actor/mention exclusions and the send-time view re-check are unchanged.
- The gate is applied PER PAGE (one
filterChannelEnabledbatch query per watcher page) — no per-watcher N+1, no load-all (finding #57). - The watcher COMMENT branch is explicitly left as-is with a PR-body finding (no silent scope creep);
pnpm test:coverageholds the ≥90% gate on the touched service.
Context refs
lib/services/watcherNotificationsService.ts—fanOut(the transition branch + the paged roster walk to gate)lib/services/notificationPreferencesService.ts—filterChannelEnabled(the batch email gate, already used by the mention job) /isChannelEnabledlib/services/mentionNotificationsService.ts— the 5.7.6 mention-job gate this mirrors (the wiring pattern)lib/notifications/preferences.ts(transitionedevent-type meta + defaults)- notes.html mistake #40 (the planning gap this closes)