feat(notifications): event→subscription→delivery service + MCP badge feed (closes #536, advances #535) - #707
Conversation
…feed (closes #536, advances #535) Closes the contribution loop's killer event: when a reviewer requests changes on a miner's PR, the miner now has a notification to read. Builds the event→subscription→delivery pipeline on top of the existing changes-requested detector (events.ts, #609), delivered through the miner's primary surface — MCP. - D1: `notification_subscriptions` (per-channel opt-out; badge on by default) + `notification_deliveries` with UNIQUE(dedup_key, channel) as the idempotency guard, so a duplicate webhook / queue retry produces exactly one delivery. - Queue: `notify-evaluate` / `notify-deliver` job types. The webhook enqueues notify-evaluate per detected event; evaluate resolves channels, writes one idempotent delivery row (rate-limited per recipient/window — bursts beyond the cap are recorded `suppressed`, never notified), and enqueues notify-deliver; deliver makes the badge row visible (pull-based). - Notification service (`src/notifications/service.ts`): channel resolution, public-safe changes-requested copy (via sanitizePublicComment), the badge feed builder (unread = delivered count), idempotent evaluate, and deliver. - MCP: `gittensory_list_notifications` + `gittensory_mark_notifications_read`, the miner's harness surface. Both self-scoped via requireContributorAccess — a session can only read/clear its OWN login's notifications. - `DetectedNotificationEvent`/`NotificationEventType` moved to types.ts (canonical location) to avoid a types↔events circular import. Scope: badge channel only (the AC's first channel, not gated behind #150/PWA). The maintainer-gated browser-extension badge UI (#534/#569), email (#570), and the predicted-gate fix-list enrichment remain follow-ups on #535. Tests: service unit (channel resolution, copy, feed, idempotency, rate-limit, mute, deliver), queue wiring (webhook→evaluate→deliver e2e + idempotency), MCP tool scope (own vs. other login). 97% coverage gate green; workers tests pass.
|
Note Gittensory Gate skippedPR closed before full evaluation. No late first comment was created.
💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers. |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
gittensory · advisory review Reviewed 11 changed file(s) — two independent AI reviewers. Suggested action: 🛠️ Request changes. (reviewers split: merge / request changes) Address the suggestions below before merging. Reviewer A · Suggestions
Worth double-checking
Reviewer B · Suggestions
Worth double-checking
|
What
Closes the contribution loop's killer event (#535): when a reviewer requests changes on a miner's PR, the miner now has a notification to read. Builds the event → subscription → delivery pipeline on top of the existing changes-requested detector (
src/notifications/events.ts, shipped in #609), delivered through the miner's primary surface — MCP.Also closes #536 (the
reviewwebhook-payload field +GitHubReviewPayloadalready landed with #609; this PR consumes them end-to-end, satisfying that prerequisite).Pipeline
migrations/0031_…):notification_subscriptions(per-channel opt-out; badge on by default) +notification_deliverieswithUNIQUE(dedup_key, channel)as the idempotency guard — a duplicate webhook / queue retry produces exactly one delivery.notify-evaluate/notify-deliverjob types. The webhook enqueuesnotify-evaluateper detected event; evaluate resolves the recipient's channels, writes one idempotent delivery row (rate-limited per recipient/window — bursts beyond the cap are recordedsuppressed, never notified), and enqueuesnotify-deliver; deliver makes the badge row visible (pull-based).src/notifications/service.ts): channel resolution, public-safe changes-requested copy (viasanitizePublicComment), the badge feed builder (unread= delivered count), idempotent evaluate, deliver.Miner surface (the product rule)
gittensory_list_notifications+gittensory_mark_notifications_read. Both self-scoped viarequireContributorAccess— a session can only read/clear its own login's notifications.routes.ts:812), so it cannot reach plain miners. Per the product rule, miners reach Gittensory through their MCP harness. The literal extension-badge UI (for maintainers-who-are-also-contributors) belongs with the extension rebuild (feat(extension): rebuild as dual-audience overlay (WXT + React) #534/feat(notifications): extension-badge delivery channel #569).Access boundary
No change to the API access-control surface. The only new reachable surface is the two MCP tools, both gated by the existing
requireContributorAccess(own-login-only). All notification copy passes throughsanitizePublicComment.Scope
Badge channel only — the AC's first channel ("not gated behind #150/PWA"). Email (#570), the browser-extension badge UI (#534/#569), and the predicted-gate fix-list enrichment remain follow-ups on #535.
Tests
notifications-service.test.ts: channel resolution + mute, copy + reviewer fallback, feed (unread = delivered only), evaluate idempotency, rate-limit suppression, deliver no-op guards, repo round-trips, queue webhook→evaluate→deliver e2e + idempotency.mcp-notifications.test.ts: tool success + scope (own vs. other login throws "authenticated GitHub login").queue.test.ts: strengthened the existing changes-requested webhook test to assert thenotify-evaluateenqueue.git diff --check+ui:openapi:checkall pass.Closes #536. Advances #535 (badge-channel slice + MCP feed).