@equilateral_ai/mindmeld 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. package/src/index.js +969 -0
@@ -0,0 +1,1032 @@
1
+ /**
2
+ * Rapport v3 - Notification Service
3
+ *
4
+ * Purpose: Sends notifications via email (SES) and Slack webhooks
5
+ *
6
+ * Notification Types:
7
+ * - Pattern promotions (pattern validated/reinforced)
8
+ * - Weekly digests (team activity summary)
9
+ * - Critical violations (standards violations)
10
+ * - Team alerts (attention alerts for managers)
11
+ * - Curation candidates (patterns ready for review)
12
+ *
13
+ * Based on: Tim-Combo pattern with environment-based configuration
14
+ */
15
+
16
+ const { SESClient, SendEmailCommand, SendTemplatedEmailCommand } = require('@aws-sdk/client-ses');
17
+
18
+ class NotificationService {
19
+ constructor(config = {}) {
20
+ this.config = {
21
+ sesRegion: config.sesRegion || process.env.AWS_REGION || 'us-east-2',
22
+ fromEmail: config.fromEmail || process.env.SES_FROM_EMAIL || 'noreply@mindmeld.dev',
23
+ fromName: config.fromName || 'MindMeld',
24
+ slackWebhookUrl: config.slackWebhookUrl || process.env.SLACK_WEBHOOK_URL,
25
+ slackCriticalWebhookUrl: config.slackCriticalWebhookUrl || process.env.SLACK_CRITICAL_WEBHOOK_URL,
26
+ appUrl: config.appUrl || process.env.APP_URL || 'https://mindmeld.dev',
27
+ enabled: config.enabled !== false && process.env.NOTIFICATIONS_ENABLED !== 'false',
28
+ ...config
29
+ };
30
+
31
+ // Initialize SES client (lazy - created once, reused across invocations)
32
+ this.sesClient = null;
33
+ }
34
+
35
+ /**
36
+ * Get SES client (cached single client pattern for Lambda)
37
+ */
38
+ getSESClient() {
39
+ if (!this.sesClient) {
40
+ this.sesClient = new SESClient({ region: this.config.sesRegion });
41
+ }
42
+ return this.sesClient;
43
+ }
44
+
45
+ /**
46
+ * Send a notification based on type and preferences
47
+ *
48
+ * @param {Object} options - Notification options
49
+ * @param {string} options.type - Notification type (pattern_promotion, weekly_digest, critical_violation, team_alert, curation_candidate)
50
+ * @param {string} options.email - Recipient email
51
+ * @param {Object} options.preferences - User notification preferences
52
+ * @param {Object} options.data - Notification-specific data
53
+ * @param {string} options.projectId - Optional project ID for project-specific preferences
54
+ * @returns {Promise<Object>} Send result
55
+ */
56
+ async sendNotification({ type, email, preferences, data, projectId }) {
57
+ if (!this.config.enabled) {
58
+ console.log('[NotificationService] Notifications disabled, skipping');
59
+ return { sent: false, reason: 'notifications_disabled' };
60
+ }
61
+
62
+ const results = { email: null, slack: null };
63
+
64
+ // Check email preferences
65
+ if (this.shouldSendEmail(type, preferences, projectId)) {
66
+ try {
67
+ results.email = await this.sendEmail(type, email, data);
68
+ } catch (error) {
69
+ console.error('[NotificationService] Email send failed:', error);
70
+ results.email = { sent: false, error: error.message };
71
+ }
72
+ }
73
+
74
+ // Check Slack preferences (for team/project notifications)
75
+ if (this.shouldSendSlack(type, preferences, projectId, data)) {
76
+ try {
77
+ results.slack = await this.sendSlack(type, data);
78
+ } catch (error) {
79
+ console.error('[NotificationService] Slack send failed:', error);
80
+ results.slack = { sent: false, error: error.message };
81
+ }
82
+ }
83
+
84
+ return results;
85
+ }
86
+
87
+ /**
88
+ * Check if email notification should be sent
89
+ */
90
+ shouldSendEmail(type, preferences, projectId) {
91
+ if (!preferences) return true; // Default to enabled if no preferences
92
+
93
+ // Check global email preference
94
+ if (preferences.email_enabled === false) return false;
95
+
96
+ // Check type-specific preference
97
+ const typePrefs = preferences.notification_types || {};
98
+ if (typePrefs[type]?.email === false) return false;
99
+
100
+ // Check project-specific preference
101
+ if (projectId && preferences.project_overrides) {
102
+ const projectPrefs = preferences.project_overrides[projectId];
103
+ if (projectPrefs?.email_enabled === false) return false;
104
+ if (projectPrefs?.notification_types?.[type]?.email === false) return false;
105
+ }
106
+
107
+ return true;
108
+ }
109
+
110
+ /**
111
+ * Check if Slack notification should be sent
112
+ */
113
+ shouldSendSlack(type, preferences, projectId, data) {
114
+ // Must have webhook URL configured
115
+ const isCritical = type === 'critical_violation' || data?.severity === 'critical';
116
+ const webhookUrl = isCritical ? this.config.slackCriticalWebhookUrl : this.config.slackWebhookUrl;
117
+
118
+ if (!webhookUrl) return false;
119
+
120
+ if (!preferences) return true; // Default to enabled if no preferences
121
+
122
+ // Check global Slack preference
123
+ if (preferences.slack_enabled === false) return false;
124
+
125
+ // Check type-specific preference
126
+ const typePrefs = preferences.notification_types || {};
127
+ if (typePrefs[type]?.slack === false) return false;
128
+
129
+ // Check project-specific preference
130
+ if (projectId && preferences.project_overrides) {
131
+ const projectPrefs = preferences.project_overrides[projectId];
132
+ if (projectPrefs?.slack_enabled === false) return false;
133
+ if (projectPrefs?.notification_types?.[type]?.slack === false) return false;
134
+ }
135
+
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Send email notification via SES
141
+ */
142
+ async sendEmail(type, email, data) {
143
+ const emailContent = this.buildEmailContent(type, data);
144
+
145
+ const command = new SendEmailCommand({
146
+ Source: `${this.config.fromName} <${this.config.fromEmail}>`,
147
+ Destination: {
148
+ ToAddresses: [email]
149
+ },
150
+ Message: {
151
+ Subject: {
152
+ Data: emailContent.subject,
153
+ Charset: 'UTF-8'
154
+ },
155
+ Body: {
156
+ Html: {
157
+ Data: emailContent.html,
158
+ Charset: 'UTF-8'
159
+ },
160
+ Text: {
161
+ Data: emailContent.text,
162
+ Charset: 'UTF-8'
163
+ }
164
+ }
165
+ }
166
+ });
167
+
168
+ const result = await this.getSESClient().send(command);
169
+
170
+ console.log(`[NotificationService] Email sent: ${type} to ${email}`);
171
+ return { sent: true, messageId: result.MessageId };
172
+ }
173
+
174
+ /**
175
+ * Build email content based on notification type
176
+ */
177
+ buildEmailContent(type, data) {
178
+ switch (type) {
179
+ case 'pattern_promotion':
180
+ return this.buildPatternPromotionEmail(data);
181
+ case 'weekly_digest':
182
+ return this.buildWeeklyDigestEmail(data);
183
+ case 'critical_violation':
184
+ return this.buildCriticalViolationEmail(data);
185
+ case 'team_alert':
186
+ return this.buildTeamAlertEmail(data);
187
+ case 'curation_candidate':
188
+ return this.buildCurationCandidateEmail(data);
189
+ default:
190
+ return this.buildGenericEmail(type, data);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Build pattern promotion email
196
+ */
197
+ buildPatternPromotionEmail(data) {
198
+ const { patternName, projectName, newMaturity, evidence } = data;
199
+
200
+ const subject = `Pattern Promoted: ${patternName}`;
201
+
202
+ const html = `
203
+ <!DOCTYPE html>
204
+ <html>
205
+ <head>
206
+ <style>
207
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
208
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
209
+ .header { background: #2563eb; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
210
+ .content { background: #f8fafc; padding: 20px; border-radius: 0 0 8px 8px; }
211
+ .metric { display: inline-block; margin: 10px 20px 10px 0; }
212
+ .metric-value { font-size: 24px; font-weight: bold; color: #2563eb; }
213
+ .metric-label { font-size: 12px; color: #64748b; }
214
+ .button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
215
+ .footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="container">
220
+ <div class="header">
221
+ <h1>Pattern Promoted</h1>
222
+ </div>
223
+ <div class="content">
224
+ <h2>${patternName}</h2>
225
+ <p>A pattern in <strong>${projectName}</strong> has been promoted to <strong>${newMaturity}</strong> status.</p>
226
+
227
+ ${evidence ? `
228
+ <div class="metrics">
229
+ <div class="metric">
230
+ <div class="metric-value">${(evidence.correlation * 100).toFixed(0)}%</div>
231
+ <div class="metric-label">Success Rate</div>
232
+ </div>
233
+ <div class="metric">
234
+ <div class="metric-value">${evidence.projectCount || 0}</div>
235
+ <div class="metric-label">Projects</div>
236
+ </div>
237
+ <div class="metric">
238
+ <div class="metric-value">${evidence.developerCount || 0}</div>
239
+ <div class="metric-label">Developers</div>
240
+ </div>
241
+ <div class="metric">
242
+ <div class="metric-value">${evidence.sessionCount || 0}</div>
243
+ <div class="metric-label">Sessions</div>
244
+ </div>
245
+ </div>
246
+ ` : ''}
247
+
248
+ <a href="${this.config.appUrl}/dashboard/patterns" class="button">View Patterns</a>
249
+ </div>
250
+ <div class="footer">
251
+ <p>You received this because you're a collaborator on ${projectName}.</p>
252
+ <p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
253
+ </div>
254
+ </div>
255
+ </body>
256
+ </html>`;
257
+
258
+ const text = `Pattern Promoted: ${patternName}
259
+
260
+ A pattern in ${projectName} has been promoted to ${newMaturity} status.
261
+
262
+ ${evidence ? `Evidence:
263
+ - Success Rate: ${(evidence.correlation * 100).toFixed(0)}%
264
+ - Projects: ${evidence.projectCount || 0}
265
+ - Developers: ${evidence.developerCount || 0}
266
+ - Sessions: ${evidence.sessionCount || 0}` : ''}
267
+
268
+ View patterns: ${this.config.appUrl}/dashboard/patterns
269
+
270
+ ---
271
+ Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
272
+
273
+ return { subject, html, text };
274
+ }
275
+
276
+ /**
277
+ * Build weekly digest email
278
+ */
279
+ buildWeeklyDigestEmail(data) {
280
+ const {
281
+ userName,
282
+ weekStart,
283
+ weekEnd,
284
+ patternsLearned,
285
+ patternsReinforced,
286
+ sessionsCount,
287
+ teamActivity,
288
+ loadBearingElements
289
+ } = data;
290
+
291
+ const subject = `Weekly Digest: ${weekStart} - ${weekEnd}`;
292
+
293
+ const html = `
294
+ <!DOCTYPE html>
295
+ <html>
296
+ <head>
297
+ <style>
298
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
299
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
300
+ .header { background: linear-gradient(135deg, #2563eb, #7c3aed); color: white; padding: 20px; border-radius: 8px 8px 0 0; }
301
+ .content { background: #f8fafc; padding: 20px; }
302
+ .section { background: white; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
303
+ .section h3 { margin-top: 0; color: #1e293b; }
304
+ .stat-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #e2e8f0; }
305
+ .stat-row:last-child { border-bottom: none; }
306
+ .stat-label { color: #64748b; }
307
+ .stat-value { font-weight: 600; color: #1e293b; }
308
+ .footer { margin-top: 20px; font-size: 12px; color: #94a3b8; text-align: center; }
309
+ </style>
310
+ </head>
311
+ <body>
312
+ <div class="container">
313
+ <div class="header">
314
+ <h1>Weekly Digest</h1>
315
+ <p>${weekStart} - ${weekEnd}</p>
316
+ </div>
317
+ <div class="content">
318
+ <p>Hi ${userName},</p>
319
+ <p>Here's your team's collaboration summary for the week.</p>
320
+
321
+ <div class="section">
322
+ <h3>Your Activity</h3>
323
+ <div class="stat-row">
324
+ <span class="stat-label">Sessions</span>
325
+ <span class="stat-value">${sessionsCount || 0}</span>
326
+ </div>
327
+ <div class="stat-row">
328
+ <span class="stat-label">Patterns Learned</span>
329
+ <span class="stat-value">${patternsLearned || 0}</span>
330
+ </div>
331
+ <div class="stat-row">
332
+ <span class="stat-label">Patterns Reinforced</span>
333
+ <span class="stat-value">${patternsReinforced || 0}</span>
334
+ </div>
335
+ </div>
336
+
337
+ ${teamActivity ? `
338
+ <div class="section">
339
+ <h3>Team Activity</h3>
340
+ <div class="stat-row">
341
+ <span class="stat-label">Total Sessions</span>
342
+ <span class="stat-value">${teamActivity.totalSessions || 0}</span>
343
+ </div>
344
+ <div class="stat-row">
345
+ <span class="stat-label">Active Collaborators</span>
346
+ <span class="stat-value">${teamActivity.activeCollaborators || 0}</span>
347
+ </div>
348
+ <div class="stat-row">
349
+ <span class="stat-label">Shared Patterns</span>
350
+ <span class="stat-value">${teamActivity.sharedPatterns || 0}</span>
351
+ </div>
352
+ </div>
353
+ ` : ''}
354
+
355
+ ${loadBearingElements && loadBearingElements.length > 0 ? `
356
+ <div class="section">
357
+ <h3>Load-Bearing Context (Top ${loadBearingElements.length})</h3>
358
+ ${loadBearingElements.map(el => `
359
+ <div class="stat-row">
360
+ <span class="stat-label">${el.key}</span>
361
+ <span class="stat-value">${(el.correlation * 100).toFixed(0)}% correlation</span>
362
+ </div>
363
+ `).join('')}
364
+ </div>
365
+ ` : ''}
366
+ </div>
367
+ <div class="footer">
368
+ <p><a href="${this.config.appUrl}/dashboard">View Full Dashboard</a></p>
369
+ <p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
370
+ </div>
371
+ </div>
372
+ </body>
373
+ </html>`;
374
+
375
+ const text = `Weekly Digest: ${weekStart} - ${weekEnd}
376
+
377
+ Hi ${userName},
378
+
379
+ Here's your team's collaboration summary for the week.
380
+
381
+ YOUR ACTIVITY
382
+ - Sessions: ${sessionsCount || 0}
383
+ - Patterns Learned: ${patternsLearned || 0}
384
+ - Patterns Reinforced: ${patternsReinforced || 0}
385
+
386
+ ${teamActivity ? `TEAM ACTIVITY
387
+ - Total Sessions: ${teamActivity.totalSessions || 0}
388
+ - Active Collaborators: ${teamActivity.activeCollaborators || 0}
389
+ - Shared Patterns: ${teamActivity.sharedPatterns || 0}` : ''}
390
+
391
+ View dashboard: ${this.config.appUrl}/dashboard
392
+
393
+ ---
394
+ Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
395
+
396
+ return { subject, html, text };
397
+ }
398
+
399
+ /**
400
+ * Build critical violation email
401
+ */
402
+ buildCriticalViolationEmail(data) {
403
+ const { violationType, projectName, standardName, details, filePath } = data;
404
+
405
+ const subject = `[CRITICAL] Standards Violation: ${standardName}`;
406
+
407
+ const html = `
408
+ <!DOCTYPE html>
409
+ <html>
410
+ <head>
411
+ <style>
412
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
413
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
414
+ .header { background: #dc2626; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
415
+ .content { background: #fef2f2; padding: 20px; border-radius: 0 0 8px 8px; }
416
+ .alert-box { background: white; border-left: 4px solid #dc2626; padding: 16px; margin: 16px 0; }
417
+ .code-block { background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 6px; font-family: monospace; overflow-x: auto; }
418
+ .button { display: inline-block; background: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
419
+ .footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
420
+ </style>
421
+ </head>
422
+ <body>
423
+ <div class="container">
424
+ <div class="header">
425
+ <h1>Critical Standards Violation</h1>
426
+ </div>
427
+ <div class="content">
428
+ <div class="alert-box">
429
+ <strong>Standard:</strong> ${standardName}<br>
430
+ <strong>Project:</strong> ${projectName}<br>
431
+ ${filePath ? `<strong>File:</strong> ${filePath}<br>` : ''}
432
+ <strong>Type:</strong> ${violationType}
433
+ </div>
434
+
435
+ <h3>Details</h3>
436
+ <p>${details}</p>
437
+
438
+ <a href="${this.config.appUrl}/dashboard/violations" class="button">View Violations</a>
439
+ </div>
440
+ <div class="footer">
441
+ <p>This is a critical alert. You received this because you're a project admin.</p>
442
+ <p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
443
+ </div>
444
+ </div>
445
+ </body>
446
+ </html>`;
447
+
448
+ const text = `[CRITICAL] Standards Violation: ${standardName}
449
+
450
+ Standard: ${standardName}
451
+ Project: ${projectName}
452
+ ${filePath ? `File: ${filePath}` : ''}
453
+ Type: ${violationType}
454
+
455
+ Details:
456
+ ${details}
457
+
458
+ View violations: ${this.config.appUrl}/dashboard/violations
459
+
460
+ ---
461
+ Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
462
+
463
+ return { subject, html, text };
464
+ }
465
+
466
+ /**
467
+ * Build team alert email
468
+ */
469
+ buildTeamAlertEmail(data) {
470
+ const { alertType, severity, userName, details, message } = data;
471
+
472
+ const severityColors = {
473
+ concern: '#dc2626',
474
+ warning: '#f59e0b',
475
+ info: '#3b82f6'
476
+ };
477
+
478
+ const subject = `[${severity.toUpperCase()}] Team Alert: ${message}`;
479
+
480
+ const html = `
481
+ <!DOCTYPE html>
482
+ <html>
483
+ <head>
484
+ <style>
485
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
486
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
487
+ .header { background: ${severityColors[severity] || '#3b82f6'}; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
488
+ .content { background: #f8fafc; padding: 20px; border-radius: 0 0 8px 8px; }
489
+ .alert-box { background: white; border-left: 4px solid ${severityColors[severity] || '#3b82f6'}; padding: 16px; margin: 16px 0; }
490
+ .button { display: inline-block; background: ${severityColors[severity] || '#3b82f6'}; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
491
+ .footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
492
+ </style>
493
+ </head>
494
+ <body>
495
+ <div class="container">
496
+ <div class="header">
497
+ <h1>Team Alert</h1>
498
+ </div>
499
+ <div class="content">
500
+ <div class="alert-box">
501
+ <strong>Team Member:</strong> ${userName}<br>
502
+ <strong>Alert Type:</strong> ${alertType}<br>
503
+ <strong>Severity:</strong> ${severity}
504
+ </div>
505
+
506
+ <h3>${message}</h3>
507
+
508
+ ${details ? `
509
+ <ul>
510
+ ${Object.entries(details).map(([key, value]) => `<li><strong>${key}:</strong> ${value}</li>`).join('')}
511
+ </ul>
512
+ ` : ''}
513
+
514
+ <a href="${this.config.appUrl}/dashboard/alerts" class="button">View Alerts</a>
515
+ </div>
516
+ <div class="footer">
517
+ <p>You received this because you're a team manager.</p>
518
+ <p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
519
+ </div>
520
+ </div>
521
+ </body>
522
+ </html>`;
523
+
524
+ const text = `[${severity.toUpperCase()}] Team Alert: ${message}
525
+
526
+ Team Member: ${userName}
527
+ Alert Type: ${alertType}
528
+ Severity: ${severity}
529
+
530
+ ${details ? Object.entries(details).map(([key, value]) => `${key}: ${value}`).join('\n') : ''}
531
+
532
+ View alerts: ${this.config.appUrl}/dashboard/alerts
533
+
534
+ ---
535
+ Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
536
+
537
+ return { subject, html, text };
538
+ }
539
+
540
+ /**
541
+ * Build curation candidate email
542
+ */
543
+ buildCurationCandidateEmail(data) {
544
+ const { candidateId, patternName, category, evidence } = data;
545
+
546
+ const subject = `New Curation Candidate: ${patternName}`;
547
+
548
+ const html = `
549
+ <!DOCTYPE html>
550
+ <html>
551
+ <head>
552
+ <style>
553
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
554
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
555
+ .header { background: linear-gradient(135deg, #059669, #10b981); color: white; padding: 20px; border-radius: 8px 8px 0 0; }
556
+ .content { background: #f0fdf4; padding: 20px; border-radius: 0 0 8px 8px; }
557
+ .metric { display: inline-block; margin: 10px 20px 10px 0; }
558
+ .metric-value { font-size: 24px; font-weight: bold; color: #059669; }
559
+ .metric-label { font-size: 12px; color: #64748b; }
560
+ .button { display: inline-block; background: #059669; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; margin-right: 10px; }
561
+ .button-secondary { background: #64748b; }
562
+ .footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
563
+ </style>
564
+ </head>
565
+ <body>
566
+ <div class="container">
567
+ <div class="header">
568
+ <h1>New Curation Candidate</h1>
569
+ <p>Pattern ready for review</p>
570
+ </div>
571
+ <div class="content">
572
+ <h2>${patternName}</h2>
573
+ <p><strong>Category:</strong> ${category}</p>
574
+
575
+ ${evidence ? `
576
+ <div class="metrics">
577
+ <div class="metric">
578
+ <div class="metric-value">${(evidence.correlation * 100).toFixed(0)}%</div>
579
+ <div class="metric-label">Success Rate</div>
580
+ </div>
581
+ <div class="metric">
582
+ <div class="metric-value">${evidence.projectCount || 0}</div>
583
+ <div class="metric-label">Projects</div>
584
+ </div>
585
+ <div class="metric">
586
+ <div class="metric-value">${evidence.developerCount || 0}</div>
587
+ <div class="metric-label">Developers</div>
588
+ </div>
589
+ <div class="metric">
590
+ <div class="metric-value">${evidence.sessionCount || 0}</div>
591
+ <div class="metric-label">Sessions</div>
592
+ </div>
593
+ </div>
594
+ ` : ''}
595
+
596
+ <p>This pattern has met all promotion criteria and is ready for human review before being promoted to .equilateral-standards/</p>
597
+
598
+ <a href="${this.config.appUrl}/admin/curation/${candidateId}" class="button">Review Candidate</a>
599
+ <a href="${this.config.appUrl}/admin/curation" class="button button-secondary">View All Candidates</a>
600
+ </div>
601
+ <div class="footer">
602
+ <p>You received this because you're a standards curator.</p>
603
+ <p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
604
+ </div>
605
+ </div>
606
+ </body>
607
+ </html>`;
608
+
609
+ const text = `New Curation Candidate: ${patternName}
610
+
611
+ Category: ${category}
612
+
613
+ ${evidence ? `Evidence:
614
+ - Success Rate: ${(evidence.correlation * 100).toFixed(0)}%
615
+ - Projects: ${evidence.projectCount || 0}
616
+ - Developers: ${evidence.developerCount || 0}
617
+ - Sessions: ${evidence.sessionCount || 0}` : ''}
618
+
619
+ This pattern has met all promotion criteria and is ready for human review.
620
+
621
+ Review candidate: ${this.config.appUrl}/admin/curation/${candidateId}
622
+ View all candidates: ${this.config.appUrl}/admin/curation
623
+
624
+ ---
625
+ Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
626
+
627
+ return { subject, html, text };
628
+ }
629
+
630
+ /**
631
+ * Build generic email
632
+ */
633
+ buildGenericEmail(type, data) {
634
+ const subject = `MindMeld Notification: ${type}`;
635
+
636
+ const html = `
637
+ <!DOCTYPE html>
638
+ <html>
639
+ <head>
640
+ <style>
641
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
642
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
643
+ .header { background: #2563eb; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
644
+ .content { background: #f8fafc; padding: 20px; border-radius: 0 0 8px 8px; }
645
+ .footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
646
+ </style>
647
+ </head>
648
+ <body>
649
+ <div class="container">
650
+ <div class="header">
651
+ <h1>MindMeld Notification</h1>
652
+ </div>
653
+ <div class="content">
654
+ <p><strong>Type:</strong> ${type}</p>
655
+ <pre>${JSON.stringify(data, null, 2)}</pre>
656
+ </div>
657
+ <div class="footer">
658
+ <p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
659
+ </div>
660
+ </div>
661
+ </body>
662
+ </html>`;
663
+
664
+ const text = `MindMeld Notification
665
+
666
+ Type: ${type}
667
+
668
+ ${JSON.stringify(data, null, 2)}
669
+
670
+ ---
671
+ Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
672
+
673
+ return { subject, html, text };
674
+ }
675
+
676
+ /**
677
+ * Send Slack notification via webhook
678
+ */
679
+ async sendSlack(type, data) {
680
+ const isCritical = type === 'critical_violation' || data?.severity === 'critical';
681
+ const webhookUrl = isCritical ? this.config.slackCriticalWebhookUrl : this.config.slackWebhookUrl;
682
+
683
+ if (!webhookUrl) {
684
+ return { sent: false, reason: 'no_webhook_url' };
685
+ }
686
+
687
+ const payload = this.buildSlackPayload(type, data);
688
+
689
+ const response = await fetch(webhookUrl, {
690
+ method: 'POST',
691
+ headers: { 'Content-Type': 'application/json' },
692
+ body: JSON.stringify(payload)
693
+ });
694
+
695
+ if (!response.ok) {
696
+ throw new Error(`Slack webhook failed: ${response.status}`);
697
+ }
698
+
699
+ console.log(`[NotificationService] Slack notification sent: ${type}`);
700
+ return { sent: true };
701
+ }
702
+
703
+ /**
704
+ * Build Slack message payload based on notification type
705
+ */
706
+ buildSlackPayload(type, data) {
707
+ switch (type) {
708
+ case 'pattern_promotion':
709
+ return this.buildSlackPatternPromotion(data);
710
+ case 'critical_violation':
711
+ return this.buildSlackCriticalViolation(data);
712
+ case 'team_alert':
713
+ return this.buildSlackTeamAlert(data);
714
+ case 'curation_candidate':
715
+ return this.buildSlackCurationCandidate(data);
716
+ default:
717
+ return this.buildSlackGeneric(type, data);
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Build Slack pattern promotion payload
723
+ */
724
+ buildSlackPatternPromotion(data) {
725
+ const { patternName, projectName, newMaturity, evidence } = data;
726
+
727
+ return {
728
+ blocks: [
729
+ {
730
+ type: 'header',
731
+ text: {
732
+ type: 'plain_text',
733
+ text: 'Pattern Promoted',
734
+ emoji: true
735
+ }
736
+ },
737
+ {
738
+ type: 'section',
739
+ fields: [
740
+ {
741
+ type: 'mrkdwn',
742
+ text: `*Pattern:*\n${patternName}`
743
+ },
744
+ {
745
+ type: 'mrkdwn',
746
+ text: `*Project:*\n${projectName}`
747
+ },
748
+ {
749
+ type: 'mrkdwn',
750
+ text: `*New Status:*\n${newMaturity}`
751
+ },
752
+ {
753
+ type: 'mrkdwn',
754
+ text: `*Success Rate:*\n${evidence ? (evidence.correlation * 100).toFixed(0) + '%' : 'N/A'}`
755
+ }
756
+ ]
757
+ },
758
+ {
759
+ type: 'actions',
760
+ elements: [
761
+ {
762
+ type: 'button',
763
+ text: {
764
+ type: 'plain_text',
765
+ text: 'View Patterns'
766
+ },
767
+ url: `${this.config.appUrl}/dashboard/patterns`
768
+ }
769
+ ]
770
+ }
771
+ ]
772
+ };
773
+ }
774
+
775
+ /**
776
+ * Build Slack critical violation payload
777
+ */
778
+ buildSlackCriticalViolation(data) {
779
+ const { violationType, projectName, standardName, details, filePath } = data;
780
+
781
+ return {
782
+ blocks: [
783
+ {
784
+ type: 'header',
785
+ text: {
786
+ type: 'plain_text',
787
+ text: 'CRITICAL: Standards Violation',
788
+ emoji: true
789
+ }
790
+ },
791
+ {
792
+ type: 'section',
793
+ text: {
794
+ type: 'mrkdwn',
795
+ text: `*${standardName}*\n${details}`
796
+ }
797
+ },
798
+ {
799
+ type: 'section',
800
+ fields: [
801
+ {
802
+ type: 'mrkdwn',
803
+ text: `*Project:*\n${projectName}`
804
+ },
805
+ {
806
+ type: 'mrkdwn',
807
+ text: `*Type:*\n${violationType}`
808
+ }
809
+ ]
810
+ },
811
+ filePath ? {
812
+ type: 'context',
813
+ elements: [
814
+ {
815
+ type: 'mrkdwn',
816
+ text: `File: \`${filePath}\``
817
+ }
818
+ ]
819
+ } : null,
820
+ {
821
+ type: 'actions',
822
+ elements: [
823
+ {
824
+ type: 'button',
825
+ text: {
826
+ type: 'plain_text',
827
+ text: 'View Violations'
828
+ },
829
+ style: 'danger',
830
+ url: `${this.config.appUrl}/dashboard/violations`
831
+ }
832
+ ]
833
+ }
834
+ ].filter(Boolean)
835
+ };
836
+ }
837
+
838
+ /**
839
+ * Build Slack team alert payload
840
+ */
841
+ buildSlackTeamAlert(data) {
842
+ const { alertType, severity, userName, message, details } = data;
843
+
844
+ const severityEmoji = {
845
+ concern: ':red_circle:',
846
+ warning: ':large_orange_circle:',
847
+ info: ':large_blue_circle:'
848
+ };
849
+
850
+ return {
851
+ blocks: [
852
+ {
853
+ type: 'header',
854
+ text: {
855
+ type: 'plain_text',
856
+ text: 'Team Alert',
857
+ emoji: true
858
+ }
859
+ },
860
+ {
861
+ type: 'section',
862
+ text: {
863
+ type: 'mrkdwn',
864
+ text: `${severityEmoji[severity] || ''} *${message}*`
865
+ }
866
+ },
867
+ {
868
+ type: 'section',
869
+ fields: [
870
+ {
871
+ type: 'mrkdwn',
872
+ text: `*Team Member:*\n${userName}`
873
+ },
874
+ {
875
+ type: 'mrkdwn',
876
+ text: `*Severity:*\n${severity}`
877
+ }
878
+ ]
879
+ },
880
+ details ? {
881
+ type: 'context',
882
+ elements: Object.entries(details).map(([key, value]) => ({
883
+ type: 'mrkdwn',
884
+ text: `*${key}:* ${value}`
885
+ }))
886
+ } : null,
887
+ {
888
+ type: 'actions',
889
+ elements: [
890
+ {
891
+ type: 'button',
892
+ text: {
893
+ type: 'plain_text',
894
+ text: 'View Alerts'
895
+ },
896
+ url: `${this.config.appUrl}/dashboard/alerts`
897
+ }
898
+ ]
899
+ }
900
+ ].filter(Boolean)
901
+ };
902
+ }
903
+
904
+ /**
905
+ * Build Slack curation candidate payload
906
+ */
907
+ buildSlackCurationCandidate(data) {
908
+ const { candidateId, patternName, category, evidence } = data;
909
+
910
+ return {
911
+ blocks: [
912
+ {
913
+ type: 'header',
914
+ text: {
915
+ type: 'plain_text',
916
+ text: 'New Curation Candidate',
917
+ emoji: true
918
+ }
919
+ },
920
+ {
921
+ type: 'section',
922
+ text: {
923
+ type: 'mrkdwn',
924
+ text: `*${patternName}*\nReady for review and promotion to .equilateral-standards/`
925
+ }
926
+ },
927
+ {
928
+ type: 'section',
929
+ fields: [
930
+ {
931
+ type: 'mrkdwn',
932
+ text: `*Category:*\n${category}`
933
+ },
934
+ {
935
+ type: 'mrkdwn',
936
+ text: `*Success Rate:*\n${evidence ? (evidence.correlation * 100).toFixed(0) + '%' : 'N/A'}`
937
+ },
938
+ {
939
+ type: 'mrkdwn',
940
+ text: `*Projects:*\n${evidence?.projectCount || 0}`
941
+ },
942
+ {
943
+ type: 'mrkdwn',
944
+ text: `*Developers:*\n${evidence?.developerCount || 0}`
945
+ }
946
+ ]
947
+ },
948
+ {
949
+ type: 'actions',
950
+ elements: [
951
+ {
952
+ type: 'button',
953
+ text: {
954
+ type: 'plain_text',
955
+ text: 'Review Candidate'
956
+ },
957
+ style: 'primary',
958
+ url: `${this.config.appUrl}/admin/curation/${candidateId}`
959
+ }
960
+ ]
961
+ }
962
+ ]
963
+ };
964
+ }
965
+
966
+ /**
967
+ * Build generic Slack payload
968
+ */
969
+ buildSlackGeneric(type, data) {
970
+ return {
971
+ blocks: [
972
+ {
973
+ type: 'header',
974
+ text: {
975
+ type: 'plain_text',
976
+ text: `MindMeld: ${type}`,
977
+ emoji: true
978
+ }
979
+ },
980
+ {
981
+ type: 'section',
982
+ text: {
983
+ type: 'mrkdwn',
984
+ text: '```' + JSON.stringify(data, null, 2) + '```'
985
+ }
986
+ }
987
+ ]
988
+ };
989
+ }
990
+
991
+ /**
992
+ * Send batch notifications (for weekly digests, etc.)
993
+ *
994
+ * @param {Array} recipients - Array of { email, preferences, data }
995
+ * @param {string} type - Notification type
996
+ * @returns {Promise<Object>} Batch results
997
+ */
998
+ async sendBatch(recipients, type) {
999
+ const results = {
1000
+ total: recipients.length,
1001
+ sent: 0,
1002
+ failed: 0,
1003
+ skipped: 0,
1004
+ errors: []
1005
+ };
1006
+
1007
+ for (const recipient of recipients) {
1008
+ try {
1009
+ const result = await this.sendNotification({
1010
+ type,
1011
+ email: recipient.email,
1012
+ preferences: recipient.preferences,
1013
+ data: recipient.data,
1014
+ projectId: recipient.projectId
1015
+ });
1016
+
1017
+ if (result.email?.sent || result.slack?.sent) {
1018
+ results.sent++;
1019
+ } else {
1020
+ results.skipped++;
1021
+ }
1022
+ } catch (error) {
1023
+ results.failed++;
1024
+ results.errors.push({ email: recipient.email, error: error.message });
1025
+ }
1026
+ }
1027
+
1028
+ return results;
1029
+ }
1030
+ }
1031
+
1032
+ module.exports = { NotificationService };