Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-322Done

5.7.10 In-app `transitioned`/`watching` fan-in delivery — add the registry entry + a `work-item/transitioned` job consumer (paged watcher roster; the in_app gate falls out)

Estimate: 34m · Depends on: 5.7.3, 5.4.5

Why this exists (the seam nobody owned — notes.html mistake #40). 5.7.3 built the fan-in as an EXTENSIBLE registry and documented the transitioned/watching entry as added "when those events land — those stories document 5.7 as their seam." But Story 5.4 (the work-item/transitioned emitter + watcher fan-out, 5.4.5) shipped BEFORE 5.7.3 existed, so NEITHER story added the entry: NOTIFICATION_FAN_IN_REGISTRY holds only mentioned / comment.created today, and watchers receive ZERO in-app rows on a transition (the drawer "Watching" tab is always empty in prod). This subtask closes that seam.

Registry entry. Add 'work-item/transitioned'{ notificationType: 'transitioned', category: 'watching', buildPlan } to NOTIFICATION_FAN_IN_REGISTRY (lib/services/notificationFanInService.ts). buildPlan resolves the recipient set from the watcher roster (watcherRepository), excludes the actor (the never-self-notify rule), sets dedupeSourceId = revisionId, and builds a data payload carrying the from/to status names the drawer renders (the fromStatus/toStatus slot the 5.7.9 shared NotificationData anticipates).

Job consumer. Add notificationFanInOnTransitioned (lib/jobs/definitions/notificationFanIn.ts, the defineJob harness) triggered on work-item/transitioned — an ADDITIONAL consumer beside the 5.4.5 watcher-notify/transitioned email job (same event, distinct id; one function per id, many functions per event), handing the event to notificationFanInService.fanIn. retryPolicy: 'transient'; idempotency per (revisionId × recipient) via the 5.7.2 (dedupeKey, recipientUserId) unique.

SCALE — page the watcher roster (finding #57). The generic fanIn path is mention-BOUNDED (candidate set = the comment's mentioned users) and walks candidates with an unpaged Promise.all. A transitioned candidate set is the FULL watcher roster — a 200-watcher issue must not build an unbounded in-memory batch nor fire 200 concurrent getCapabilities checks. So the transitioned fan-in MUST read + process the roster in bounded pages, mirroring watcherNotificationsService.fanOut's WATCHER_FAN_OUT_PAGE_SIZE cursor walk. Either extend fanIn to accept a paged candidate source, or give the watching-category descriptor its own paged path — pick the durable shape, not a load-all.

The in_app preference gate falls out for free. The generic fan-in already calls notificationPreferencesService.isChannelEnabled(userId, descriptor.notificationType, 'in_app') per recipient, so once the descriptor exists, a user who turned transitioned · in_app OFF gets no row — no extra gate wiring here (the matrix flip is 5.7.12; the email-channel twin is 5.7.11).

Acceptance criteria

  • A work-item/transitioned event writes ONE watching-category Notification row per watcher (actor excluded; view access re-validated AS each watcher at fan-in time), with the from/to-status data payload; a vanished issue → clean no-op.
  • The roster is walked in BOUNDED pages (no load-all Promise.all over the full roster); a 200-watcher issue never builds an unbounded batch (finding #57).
  • Turning transitioned · in_app OFF suppresses the row; default-on (unset) still writes it — verified by an integration test (mirror notifications-journey.test.ts; the existing SYNTHETIC watching descriptor test can become a real-registry assertion).
  • Idempotent per (revisionId × recipient): replay / retry never double-writes; failures land in the DLQ. No emit site is touched; no second "also notify" call is added beside the email send.
  • pnpm test:coverage holds the ≥90% gate on the touched fan-in service + job files.

Context refs

  • lib/services/notificationFanInService.tsNOTIFICATION_FAN_IN_REGISTRY + the generic fanIn (the per-recipient isChannelEnabled('in_app') gate already there)
  • lib/jobs/definitions/notificationFanIn.ts (the consumers to extend) + lib/jobs/definitions/watcherNotify.ts (the 5.4.5 email twin — same event, the second-consumer pattern) + lib/jobs/types.ts (WorkItemTransitionedData)
  • lib/repositories/watcherRepository.ts + watcherNotificationsService.fanOut — the PAGED roster walk to mirror (WATCHER_FAN_OUT_PAGE_SIZE, finding #57)
  • lib/dto/notifications.ts NotificationData (the shared shape from 5.7.9 — the fromStatus/toStatus slot) + tests/integration/notifications-journey.test.ts (the synthetic watching seam test to make real)
  • notes.html mistake #40 (the planning gap this closes)