@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,688 @@
1
+ /**
2
+ * Rapport v3 - Curation Engine
3
+ *
4
+ * Purpose: Promotes validated patterns to .equilateral-standards/
5
+ *
6
+ * Promotion Criteria:
7
+ * - correlation >= 0.90 (90% success rate)
8
+ * - projectCount >= 5 (proven across 5+ projects)
9
+ * - developerCount >= 3 (used by 3+ developers)
10
+ * - sessionCount >= 10 (reinforced 10+ times)
11
+ * - hasMeasurableBenefit = true (cost/performance gain)
12
+ *
13
+ * Based on: /Users/jamesford/Source/rapport/docs/RAPPORT_STANDARDS_INTEGRATION_DESIGN.md
14
+ */
15
+
16
+ const { executeQuery } = require('../handlers/helpers/dbOperations');
17
+ const { NotificationService } = require('./NotificationService');
18
+
19
+ class CurationEngine {
20
+ constructor(config = {}) {
21
+ this.config = {
22
+ minCorrelation: config.minCorrelation || 0.90,
23
+ minProjects: config.minProjects || 5,
24
+ minDevelopers: config.minDevelopers || 3,
25
+ minSessions: config.minSessions || 10,
26
+ requireMeasurableBenefit: config.requireMeasurableBenefit !== false,
27
+ ...config
28
+ };
29
+
30
+ // Initialize notification service
31
+ this.notificationService = config.notificationService || new NotificationService();
32
+ }
33
+
34
+ /**
35
+ * Evaluate if a pattern meets promotion criteria
36
+ *
37
+ * @param {string} patternId - Pattern identifier
38
+ * @returns {Promise<Object|null>} Curation candidate or null
39
+ */
40
+ async evaluateForPromotion(patternId) {
41
+ try {
42
+ // Get pattern metrics from materialized view
43
+ const metrics = await this.getPatternMetrics(patternId);
44
+
45
+ if (!metrics) {
46
+ console.log(`[CurationEngine] Pattern ${patternId} not found in metrics`);
47
+ return null;
48
+ }
49
+
50
+ // Check if pattern meets all criteria
51
+ if (!this.meetsAllCriteria(metrics)) {
52
+ console.log(`[CurationEngine] Pattern ${patternId} does not meet promotion criteria`);
53
+ return null;
54
+ }
55
+
56
+ // Create curation candidate
57
+ const candidate = await this.createCurationCandidate(patternId, metrics);
58
+
59
+ console.log(`[CurationEngine] Pattern ${patternId} promoted to curation candidate`);
60
+ return candidate;
61
+ } catch (error) {
62
+ console.error('[CurationEngine] Error evaluating pattern for promotion:', error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get pattern metrics from materialized view
69
+ *
70
+ * @param {string} patternId - Pattern identifier
71
+ * @returns {Promise<Object|null>} Pattern metrics
72
+ */
73
+ async getPatternMetrics(patternId) {
74
+ const query = `
75
+ SELECT
76
+ pattern_id,
77
+ element,
78
+ project_count,
79
+ developer_count,
80
+ session_count,
81
+ success_correlation,
82
+ last_used
83
+ FROM rapport.pattern_metrics
84
+ WHERE pattern_id = $1
85
+ `;
86
+
87
+ const result = await executeQuery(query, [patternId]);
88
+ return result.rows.length > 0 ? result.rows[0] : null;
89
+ }
90
+
91
+ /**
92
+ * Check if metrics meet all promotion criteria
93
+ *
94
+ * @param {Object} metrics - Pattern metrics
95
+ * @returns {boolean} True if all criteria met
96
+ */
97
+ meetsAllCriteria(metrics) {
98
+ // PostgreSQL returns numeric values as strings - parse them
99
+ const correlation = parseFloat(metrics.success_correlation) || 0;
100
+ const projectCount = parseInt(metrics.project_count) || 0;
101
+ const developerCount = parseInt(metrics.developer_count) || 0;
102
+ const sessionCount = parseInt(metrics.session_count) || 0;
103
+
104
+ const checks = {
105
+ correlation: correlation >= this.config.minCorrelation,
106
+ projects: projectCount >= this.config.minProjects,
107
+ developers: developerCount >= this.config.minDevelopers,
108
+ sessions: sessionCount >= this.config.minSessions
109
+ };
110
+
111
+ const allMet = Object.values(checks).every(check => check === true);
112
+
113
+ if (!allMet) {
114
+ console.log('[CurationEngine] Criteria check:', {
115
+ pattern: metrics.pattern_id,
116
+ correlation: `${correlation.toFixed(2)} (required: ${this.config.minCorrelation})`,
117
+ projects: `${projectCount} (required: ${this.config.minProjects})`,
118
+ developers: `${developerCount} (required: ${this.config.minDevelopers})`,
119
+ sessions: `${sessionCount} (required: ${this.config.minSessions})`,
120
+ passed: checks
121
+ });
122
+ }
123
+
124
+ return allMet;
125
+ }
126
+
127
+ /**
128
+ * Create a curation candidate for human review
129
+ *
130
+ * @param {string} patternId - Pattern identifier
131
+ * @param {Object} metrics - Pattern metrics
132
+ * @returns {Promise<Object>} Curation candidate
133
+ */
134
+ async createCurationCandidate(patternId, metrics) {
135
+ // Get pattern details
136
+ const patternDetails = await this.getPatternDetails(patternId);
137
+
138
+ // Infer category based on pattern characteristics
139
+ const category = await this.inferCategory(patternDetails);
140
+
141
+ // Gather best examples from successful usage
142
+ const examples = await this.gatherBestExamples(patternId);
143
+
144
+ // Gather anti-patterns from failed usage
145
+ const antiPatterns = await this.gatherAntiPatterns(patternId);
146
+
147
+ // Get measurable benefit data
148
+ const benefit = await this.getMeasurableBenefit(patternId);
149
+
150
+ // Build proposed standard
151
+ const proposedStandard = {
152
+ name: metrics.element,
153
+ category: category,
154
+ status: 'candidate',
155
+ evidence: {
156
+ correlation: parseFloat(metrics.success_correlation),
157
+ projectCount: metrics.project_count,
158
+ developerCount: metrics.developer_count,
159
+ sessionCount: metrics.session_count,
160
+ benefit: benefit
161
+ },
162
+ pattern: patternDetails,
163
+ examples: examples,
164
+ antiPatterns: antiPatterns,
165
+ proposedEnforcement: this.generateEnforcementRule(patternDetails)
166
+ };
167
+
168
+ // Generate standard document
169
+ const standardDocument = this.generateStandardDocument(proposedStandard);
170
+
171
+ // Save to curation database
172
+ const candidateId = await this.saveCurationCandidate(
173
+ patternId,
174
+ category,
175
+ proposedStandard.evidence,
176
+ standardDocument
177
+ );
178
+
179
+ // Notify for human review
180
+ await this.notifyForReview(candidateId, proposedStandard);
181
+
182
+ return {
183
+ candidateId,
184
+ patternId,
185
+ ...proposedStandard
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Get detailed pattern information
191
+ *
192
+ * @param {string} patternId - Pattern identifier
193
+ * @returns {Promise<Object>} Pattern details
194
+ */
195
+ async getPatternDetails(patternId) {
196
+ const query = `
197
+ SELECT
198
+ pattern_id,
199
+ project_id,
200
+ intent,
201
+ constraints,
202
+ outcome_criteria,
203
+ maturity,
204
+ handoff_count,
205
+ successful_handoffs,
206
+ failed_handoffs,
207
+ discovered_by,
208
+ discovered_at,
209
+ last_used,
210
+ pattern_data
211
+ FROM rapport.patterns
212
+ WHERE pattern_id = $1
213
+ `;
214
+
215
+ const result = await executeQuery(query, [patternId]);
216
+ return result.rows[0];
217
+ }
218
+
219
+ /**
220
+ * Infer category for the pattern based on content analysis
221
+ *
222
+ * @param {Object} pattern - Pattern details
223
+ * @returns {Promise<string>} Inferred category
224
+ */
225
+ async inferCategory(pattern) {
226
+ const intent = pattern.intent.toLowerCase();
227
+ const constraints = JSON.stringify(pattern.constraints).toLowerCase();
228
+ const data = JSON.stringify(pattern.pattern_data).toLowerCase();
229
+
230
+ // Category detection based on keywords
231
+ const categories = {
232
+ 'serverless-saas-aws': ['lambda', 'api gateway', 'sam', 'serverless', 'handler', 'aws'],
233
+ 'database-patterns': ['database', 'query', 'postgresql', 'sql', 'schema'],
234
+ 'multi-agent-orchestration': ['agent', 'orchestration', 'workflow', 'task', 'team'],
235
+ 'frontend-development': ['react', 'component', 'ui', 'frontend', 'tsx', 'jsx'],
236
+ 'compliance-security': ['security', 'compliance', 'audit', 'gdpr', 'encryption'],
237
+ 'cost-optimization': ['cost', 'performance', 'optimization', 'billing', 'pricing']
238
+ };
239
+
240
+ const allText = `${intent} ${constraints} ${data}`;
241
+
242
+ for (const [category, keywords] of Object.entries(categories)) {
243
+ if (keywords.some(keyword => allText.includes(keyword))) {
244
+ return category;
245
+ }
246
+ }
247
+
248
+ return 'general'; // Default category
249
+ }
250
+
251
+ /**
252
+ * Gather best examples from successful pattern usage
253
+ *
254
+ * @param {string} patternId - Pattern identifier
255
+ * @returns {Promise<Array>} Best examples
256
+ */
257
+ async gatherBestExamples(patternId) {
258
+ const query = `
259
+ SELECT
260
+ pu.context,
261
+ pu.used_at,
262
+ u."User_Display_Name" as user_name,
263
+ p.project_name
264
+ FROM rapport.pattern_usage pu
265
+ JOIN "Users" u ON pu.email_address = u."Email_Address"
266
+ JOIN rapport.patterns pat ON pu.pattern_id = pat.pattern_id
267
+ JOIN rapport.projects p ON pat.project_id = p.project_id
268
+ WHERE pu.pattern_id = $1
269
+ AND pu.success = true
270
+ ORDER BY pu.used_at DESC
271
+ LIMIT 5
272
+ `;
273
+
274
+ const result = await executeQuery(query, [patternId]);
275
+
276
+ return result.rows.map(row => ({
277
+ code: row.context.code || 'No code example available',
278
+ description: row.context.description || 'Successful pattern application',
279
+ project: row.project_name,
280
+ user: row.user_name,
281
+ timestamp: row.used_at
282
+ }));
283
+ }
284
+
285
+ /**
286
+ * Gather anti-patterns from failed usage
287
+ *
288
+ * @param {string} patternId - Pattern identifier
289
+ * @returns {Promise<Array>} Anti-patterns
290
+ */
291
+ async gatherAntiPatterns(patternId) {
292
+ const query = `
293
+ SELECT
294
+ pu.context,
295
+ pu.used_at,
296
+ u."User_Display_Name" as user_name
297
+ FROM rapport.pattern_usage pu
298
+ JOIN "Users" u ON pu.email_address = u."Email_Address"
299
+ WHERE pu.pattern_id = $1
300
+ AND pu.success = false
301
+ ORDER BY pu.used_at DESC
302
+ LIMIT 5
303
+ `;
304
+
305
+ const result = await executeQuery(query, [patternId]);
306
+
307
+ return result.rows.map(row => ({
308
+ description: row.context.error || 'Pattern application failed',
309
+ context: row.context.failureReason || 'Unknown failure reason',
310
+ user: row.user_name,
311
+ timestamp: row.used_at
312
+ }));
313
+ }
314
+
315
+ /**
316
+ * Get measurable benefit data (cost savings, performance improvements)
317
+ *
318
+ * @param {string} patternId - Pattern identifier
319
+ * @returns {Promise<Object>} Measurable benefit
320
+ */
321
+ async getMeasurableBenefit(patternId) {
322
+ const query = `
323
+ SELECT
324
+ pattern_data->'benefit' as benefit_data
325
+ FROM rapport.patterns
326
+ WHERE pattern_id = $1
327
+ `;
328
+
329
+ const result = await executeQuery(query, [patternId]);
330
+
331
+ if (result.rows.length > 0 && result.rows[0].benefit_data) {
332
+ return result.rows[0].benefit_data;
333
+ }
334
+
335
+ // Default benefit if not recorded
336
+ return {
337
+ type: 'team_efficiency',
338
+ description: 'Proven pattern with high success rate across multiple teams',
339
+ quantified: false
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Generate proposed enforcement rule for the pattern
345
+ *
346
+ * @param {Object} pattern - Pattern details
347
+ * @returns {string} Enforcement rule
348
+ */
349
+ generateEnforcementRule(pattern) {
350
+ const constraints = Array.isArray(pattern.constraints) ? pattern.constraints : [];
351
+ const outcomeCriteria = Array.isArray(pattern.outcome_criteria) ? pattern.outcome_criteria : [];
352
+
353
+ let enforcement = `## Enforcement Rule\n\n`;
354
+ enforcement += `**Intent**: ${pattern.intent}\n\n`;
355
+
356
+ if (constraints.length > 0) {
357
+ enforcement += `### Required Constraints\n\n`;
358
+ constraints.forEach((constraint, idx) => {
359
+ enforcement += `${idx + 1}. ${constraint}\n`;
360
+ });
361
+ enforcement += `\n`;
362
+ }
363
+
364
+ if (outcomeCriteria.length > 0) {
365
+ enforcement += `### Success Criteria\n\n`;
366
+ outcomeCriteria.forEach((criteria, idx) => {
367
+ enforcement += `${idx + 1}. ${criteria}\n`;
368
+ });
369
+ enforcement += `\n`;
370
+ }
371
+
372
+ enforcement += `### Validation\n\n`;
373
+ enforcement += `This pattern should be automatically validated during code review and CI/CD pipeline.\n`;
374
+
375
+ return enforcement;
376
+ }
377
+
378
+ /**
379
+ * Generate markdown standard document
380
+ *
381
+ * @param {Object} candidate - Curation candidate
382
+ * @returns {string} Standard document in markdown
383
+ */
384
+ generateStandardDocument(candidate) {
385
+ const { name, category, evidence, pattern, examples, antiPatterns, proposedEnforcement } = candidate;
386
+
387
+ let doc = `# ${name}\n\n`;
388
+ doc += `**Status**: Candidate (from Rapport curation)\n`;
389
+ doc += `**Category**: ${category}\n`;
390
+ doc += `**Date**: ${new Date().toISOString()}\n\n`;
391
+
392
+ doc += `## Evidence\n\n`;
393
+ doc += `- **Correlation**: ${(evidence.correlation * 100).toFixed(1)}%\n`;
394
+ doc += `- **Projects**: ${evidence.projectCount}\n`;
395
+ doc += `- **Developers**: ${evidence.developerCount}\n`;
396
+ doc += `- **Sessions**: ${evidence.sessionCount}\n`;
397
+ doc += `- **Benefit**: ${evidence.benefit.description || JSON.stringify(evidence.benefit)}\n\n`;
398
+
399
+ doc += `## Pattern\n\n`;
400
+ doc += `**Intent**: ${pattern.intent}\n\n`;
401
+
402
+ if (pattern.constraints && pattern.constraints.length > 0) {
403
+ doc += `**Constraints**:\n`;
404
+ pattern.constraints.forEach((constraint, idx) => {
405
+ doc += `${idx + 1}. ${constraint}\n`;
406
+ });
407
+ doc += `\n`;
408
+ }
409
+
410
+ if (pattern.outcome_criteria && pattern.outcome_criteria.length > 0) {
411
+ doc += `**Outcome Criteria**:\n`;
412
+ pattern.outcome_criteria.forEach((criteria, idx) => {
413
+ doc += `${idx + 1}. ${criteria}\n`;
414
+ });
415
+ doc += `\n`;
416
+ }
417
+
418
+ doc += `## Examples\n\n`;
419
+ if (examples.length > 0) {
420
+ examples.forEach((example, idx) => {
421
+ doc += `### Example ${idx + 1}: ${example.description}\n\n`;
422
+ doc += `**Project**: ${example.project}\n`;
423
+ doc += `**User**: ${example.user}\n\n`;
424
+ doc += `\`\`\`javascript\n${example.code}\n\`\`\`\n\n`;
425
+ });
426
+ } else {
427
+ doc += `No examples available.\n\n`;
428
+ }
429
+
430
+ doc += `## Anti-Patterns\n\n`;
431
+ if (antiPatterns.length > 0) {
432
+ antiPatterns.forEach((antiPattern, idx) => {
433
+ doc += `${idx + 1}. ❌ **${antiPattern.description}**\n`;
434
+ doc += ` - Context: ${antiPattern.context}\n`;
435
+ doc += ` - Observed by: ${antiPattern.user}\n\n`;
436
+ });
437
+ } else {
438
+ doc += `No anti-patterns recorded.\n\n`;
439
+ }
440
+
441
+ doc += proposedEnforcement;
442
+ doc += `\n---\n\n`;
443
+ doc += `**Generated by**: Rapport Curation Engine\n`;
444
+ doc += `**Requires**: Human review before promotion to .equilateral-standards/\n`;
445
+
446
+ return doc;
447
+ }
448
+
449
+ /**
450
+ * Save curation candidate to database
451
+ *
452
+ * @param {string} patternId - Pattern identifier
453
+ * @param {string} category - Proposed category
454
+ * @param {Object} evidence - Evidence data
455
+ * @param {string} standardDocument - Generated standard document
456
+ * @returns {Promise<number>} Candidate ID
457
+ */
458
+ async saveCurationCandidate(patternId, category, evidence, standardDocument) {
459
+ const query = `
460
+ INSERT INTO rapport.curation_candidates
461
+ (pattern_id, proposed_category, evidence, standard_document, status)
462
+ VALUES
463
+ ($1, $2, $3, $4, 'pending')
464
+ RETURNING candidate_id
465
+ `;
466
+
467
+ const result = await executeQuery(query, [
468
+ patternId,
469
+ category,
470
+ JSON.stringify(evidence),
471
+ standardDocument
472
+ ]);
473
+
474
+ return result.rows[0].candidate_id;
475
+ }
476
+
477
+ /**
478
+ * Notify stakeholders for human review
479
+ *
480
+ * @param {number} candidateId - Candidate ID
481
+ * @param {Object} candidate - Curation candidate
482
+ */
483
+ async notifyForReview(candidateId, candidate) {
484
+ console.log(`\n========================================`);
485
+ console.log(`[CurationEngine] NEW CURATION CANDIDATE`);
486
+ console.log(`========================================`);
487
+ console.log(`Candidate ID: ${candidateId}`);
488
+ console.log(`Pattern: ${candidate.name}`);
489
+ console.log(`Category: ${candidate.category}`);
490
+ console.log(`Evidence:`);
491
+ console.log(` - Correlation: ${(candidate.evidence.correlation * 100).toFixed(1)}%`);
492
+ console.log(` - Projects: ${candidate.evidence.projectCount}`);
493
+ console.log(` - Developers: ${candidate.evidence.developerCount}`);
494
+ console.log(` - Sessions: ${candidate.evidence.sessionCount}`);
495
+ console.log(`\nAction Required: Review candidate for promotion to .equilateral-standards/`);
496
+ console.log(`========================================\n`);
497
+
498
+ // Send notifications to curators (admins with curation permissions)
499
+ try {
500
+ const curators = await this.getCurators();
501
+
502
+ for (const curator of curators) {
503
+ const preferences = await this.getUserPreferences(curator.email_address);
504
+
505
+ await this.notificationService.sendNotification({
506
+ type: 'curation_candidate',
507
+ email: curator.email_address,
508
+ preferences: preferences,
509
+ data: {
510
+ candidateId: candidateId,
511
+ patternName: candidate.name,
512
+ category: candidate.category,
513
+ evidence: candidate.evidence,
514
+ referenceType: 'curation_candidate',
515
+ referenceId: String(candidateId)
516
+ }
517
+ });
518
+ }
519
+
520
+ console.log(`[CurationEngine] Notified ${curators.length} curators`);
521
+ } catch (error) {
522
+ console.error('[CurationEngine] Failed to send notifications:', error);
523
+ // Don't throw - notification failure shouldn't block curation
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Get users with curation permissions (Super Admins and Admins)
529
+ *
530
+ * @returns {Promise<Array>} List of curator emails
531
+ */
532
+ async getCurators() {
533
+ const query = `
534
+ SELECT DISTINCT u."Email_Address" as email_address
535
+ FROM "Users" u
536
+ LEFT JOIN "UserEntitlements" ue ON u."Email_Address" = ue."Email_Address"
537
+ WHERE u."Super_Admin" = true OR ue."Admin" = true
538
+ AND u."active" = true
539
+ `;
540
+
541
+ const result = await executeQuery(query);
542
+ return result.rows;
543
+ }
544
+
545
+ /**
546
+ * Get user notification preferences
547
+ *
548
+ * @param {string} email - User email
549
+ * @returns {Promise<Object>} User preferences
550
+ */
551
+ async getUserPreferences(email) {
552
+ const query = `SELECT rapport.get_notification_preferences($1) as preferences`;
553
+ const result = await executeQuery(query, [email]);
554
+ return result.rows[0]?.preferences || null;
555
+ }
556
+
557
+ /**
558
+ * Refresh pattern metrics materialized view
559
+ *
560
+ * @returns {Promise<void>}
561
+ */
562
+ async refreshMetrics() {
563
+ try {
564
+ await executeQuery('REFRESH MATERIALIZED VIEW rapport.pattern_metrics');
565
+ console.log('[CurationEngine] Pattern metrics refreshed');
566
+ } catch (error) {
567
+ console.error('[CurationEngine] Error refreshing metrics:', error);
568
+ throw error;
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Get all pending curation candidates
574
+ *
575
+ * @returns {Promise<Array>} Pending candidates
576
+ */
577
+ async getPendingCandidates() {
578
+ const query = `
579
+ SELECT
580
+ candidate_id,
581
+ pattern_id,
582
+ proposed_category,
583
+ evidence,
584
+ standard_document,
585
+ created_at
586
+ FROM rapport.curation_candidates
587
+ WHERE status = 'pending'
588
+ ORDER BY created_at DESC
589
+ `;
590
+
591
+ const result = await executeQuery(query);
592
+ return result.rows;
593
+ }
594
+
595
+ /**
596
+ * Approve a curation candidate
597
+ *
598
+ * @param {number} candidateId - Candidate ID
599
+ * @param {string} reviewedBy - Reviewer email
600
+ * @returns {Promise<Object>} Approved candidate
601
+ */
602
+ async approveCandidate(candidateId, reviewedBy) {
603
+ const query = `
604
+ UPDATE rapport.curation_candidates
605
+ SET
606
+ status = 'approved',
607
+ reviewed_by = $2,
608
+ reviewed_at = NOW()
609
+ WHERE candidate_id = $1
610
+ RETURNING *
611
+ `;
612
+
613
+ const result = await executeQuery(query, [candidateId, reviewedBy]);
614
+ return result.rows[0];
615
+ }
616
+
617
+ /**
618
+ * Reject a curation candidate
619
+ *
620
+ * @param {number} candidateId - Candidate ID
621
+ * @param {string} reviewedBy - Reviewer email
622
+ * @returns {Promise<Object>} Rejected candidate
623
+ */
624
+ async rejectCandidate(candidateId, reviewedBy) {
625
+ const query = `
626
+ UPDATE rapport.curation_candidates
627
+ SET
628
+ status = 'rejected',
629
+ reviewed_by = $2,
630
+ reviewed_at = NOW()
631
+ WHERE candidate_id = $1
632
+ RETURNING *
633
+ `;
634
+
635
+ const result = await executeQuery(query, [candidateId, reviewedBy]);
636
+ return result.rows[0];
637
+ }
638
+
639
+ /**
640
+ * Evaluate all patterns for promotion
641
+ *
642
+ * @returns {Promise<Array>} Created candidates
643
+ */
644
+ async evaluateAllPatterns() {
645
+ try {
646
+ // Refresh metrics first
647
+ await this.refreshMetrics();
648
+
649
+ // Get all patterns that meet criteria
650
+ const query = `
651
+ SELECT pattern_id
652
+ FROM rapport.pattern_metrics
653
+ WHERE success_correlation >= $1
654
+ AND project_count >= $2
655
+ AND developer_count >= $3
656
+ AND session_count >= $4
657
+ AND pattern_id NOT IN (
658
+ SELECT pattern_id
659
+ FROM rapport.curation_candidates
660
+ WHERE status IN ('pending', 'approved')
661
+ )
662
+ `;
663
+
664
+ const result = await executeQuery(query, [
665
+ this.config.minCorrelation,
666
+ this.config.minProjects,
667
+ this.config.minDevelopers,
668
+ this.config.minSessions
669
+ ]);
670
+
671
+ const candidates = [];
672
+ for (const row of result.rows) {
673
+ const candidate = await this.evaluateForPromotion(row.pattern_id);
674
+ if (candidate) {
675
+ candidates.push(candidate);
676
+ }
677
+ }
678
+
679
+ console.log(`[CurationEngine] Evaluated ${result.rows.length} patterns, created ${candidates.length} candidates`);
680
+ return candidates;
681
+ } catch (error) {
682
+ console.error('[CurationEngine] Error evaluating all patterns:', error);
683
+ throw error;
684
+ }
685
+ }
686
+ }
687
+
688
+ module.exports = { CurationEngine };