@equilateral_ai/mindmeld 3.5.2 → 4.0.1

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 (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,1032 +0,0 @@
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 };