@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,774 @@
1
+ /**
2
+ * Rapport v3 - Correlation Analyzer
3
+ *
4
+ * Purpose: Correlates Claude Code sessions with git commits to measure productivity
5
+ *
6
+ * Key Metrics:
7
+ * - Session-to-commit conversion rate (sessions that produce commits)
8
+ * - Time between session and commit (latency)
9
+ * - Pattern success correlation (which patterns lead to commits)
10
+ * - Struggle detection (sessions without commits)
11
+ *
12
+ * Correlation Algorithm:
13
+ * 1. Find commits within a time window after session end
14
+ * 2. Match by email address (session user = commit author)
15
+ * 3. Consider branch/project alignment if available
16
+ * 4. Handle async commit behavior (commits hours/days after session)
17
+ *
18
+ * Based on: Phase 7 Engineering Intelligence
19
+ */
20
+
21
+ const { executeQuery } = require('../handlers/helpers/dbOperations');
22
+
23
+ /**
24
+ * Default configuration for correlation analysis
25
+ */
26
+ const DEFAULT_CONFIG = {
27
+ // Time window for matching sessions to commits (hours)
28
+ correlationWindowHours: 4,
29
+
30
+ // Extended window for async commits (hours)
31
+ asyncWindowHours: 24,
32
+
33
+ // Minimum session duration to consider (seconds)
34
+ minSessionDuration: 60,
35
+
36
+ // Weight factors for correlation scoring
37
+ weights: {
38
+ timeProximity: 0.4, // Closer in time = higher weight
39
+ branchMatch: 0.3, // Same branch = higher weight
40
+ projectMatch: 0.2, // Same project = higher weight
41
+ patternSuccess: 0.1 // Successful patterns = higher weight
42
+ },
43
+
44
+ // Thresholds for classification
45
+ thresholds: {
46
+ // Sessions without commits within window are "unproductive"
47
+ unproductiveWindowHours: 8,
48
+
49
+ // Conversion rate thresholds
50
+ lowConversionRate: 0.2, // Below 20% = low
51
+ highConversionRate: 0.7, // Above 70% = high
52
+
53
+ // Minimum sessions for statistical significance
54
+ minSessionsForStats: 5
55
+ }
56
+ };
57
+
58
+ class CorrelationAnalyzer {
59
+ constructor(config = {}) {
60
+ this.config = this.mergeConfig(DEFAULT_CONFIG, config);
61
+ }
62
+
63
+ /**
64
+ * Deep merge configuration
65
+ */
66
+ mergeConfig(defaults, overrides) {
67
+ const result = { ...defaults };
68
+ for (const key of Object.keys(overrides)) {
69
+ if (typeof overrides[key] === 'object' && !Array.isArray(overrides[key])) {
70
+ result[key] = { ...defaults[key], ...overrides[key] };
71
+ } else {
72
+ result[key] = overrides[key];
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Analyze correlations for all uncorrelated sessions
80
+ *
81
+ * @param {Object} options - Analysis options
82
+ * @param {string} options.projectId - Optional project filter
83
+ * @param {string} options.companyId - Optional company filter
84
+ * @param {number} options.lookbackDays - Days to look back (default: 30)
85
+ * @returns {Promise<Object>} Analysis summary
86
+ */
87
+ async analyzeCorrelations(options = {}) {
88
+ const lookbackDays = options.lookbackDays || 30;
89
+ const projectId = options.projectId;
90
+ const companyId = options.companyId;
91
+
92
+ const summary = {
93
+ startedAt: new Date().toISOString(),
94
+ sessionsAnalyzed: 0,
95
+ correlationsCreated: 0,
96
+ uncorrelatedSessions: 0,
97
+ patternCorrelations: 0,
98
+ errors: []
99
+ };
100
+
101
+ try {
102
+ console.log('[CorrelationAnalyzer] Starting correlation analysis...');
103
+
104
+ // Get uncorrelated sessions
105
+ const sessions = await this.getUncorrelatedSessions(lookbackDays, projectId, companyId);
106
+ summary.sessionsAnalyzed = sessions.length;
107
+
108
+ if (sessions.length === 0) {
109
+ console.log('[CorrelationAnalyzer] No uncorrelated sessions found');
110
+ summary.completedAt = new Date().toISOString();
111
+ return summary;
112
+ }
113
+
114
+ console.log(`[CorrelationAnalyzer] Found ${sessions.length} sessions to analyze`);
115
+
116
+ // Process each session
117
+ for (const session of sessions) {
118
+ try {
119
+ const correlation = await this.correlateSession(session);
120
+
121
+ if (correlation.hasCommits) {
122
+ await this.saveCorrelation(correlation);
123
+ summary.correlationsCreated++;
124
+
125
+ // Update pattern success rates
126
+ if (correlation.patternsUsed && correlation.patternsUsed.length > 0) {
127
+ await this.updatePatternSuccess(correlation);
128
+ summary.patternCorrelations++;
129
+ }
130
+ } else {
131
+ // Mark session as analyzed but uncorrelated
132
+ await this.markSessionAnalyzed(session.session_id, false);
133
+ summary.uncorrelatedSessions++;
134
+ }
135
+ } catch (error) {
136
+ console.error(`[CorrelationAnalyzer] Error correlating session ${session.session_id}:`, error);
137
+ summary.errors.push({
138
+ sessionId: session.session_id,
139
+ error: error.message
140
+ });
141
+ }
142
+ }
143
+
144
+ // Calculate aggregate metrics
145
+ await this.calculateAggregateMetrics(projectId, companyId);
146
+
147
+ summary.completedAt = new Date().toISOString();
148
+ console.log(`[CorrelationAnalyzer] Analysis complete: ${summary.correlationsCreated} correlations, ${summary.uncorrelatedSessions} uncorrelated`);
149
+
150
+ } catch (error) {
151
+ console.error('[CorrelationAnalyzer] Error in correlation analysis:', error);
152
+ summary.errors.push(error.message);
153
+ }
154
+
155
+ return summary;
156
+ }
157
+
158
+ /**
159
+ * Get sessions that haven't been correlated yet
160
+ *
161
+ * @param {number} lookbackDays - Days to look back
162
+ * @param {string} projectId - Optional project filter
163
+ * @param {string} companyId - Optional company filter
164
+ * @returns {Promise<Array>} Uncorrelated sessions
165
+ */
166
+ async getUncorrelatedSessions(lookbackDays, projectId = null, companyId = null) {
167
+ const query = `
168
+ SELECT
169
+ s.session_id,
170
+ s.project_id,
171
+ s.email_address,
172
+ s.started_at,
173
+ s.ended_at,
174
+ s.duration_seconds,
175
+ s.git_branch,
176
+ s.patterns_used,
177
+ s.session_data,
178
+ p.company_id,
179
+ p.repo_url
180
+ FROM rapport.sessions s
181
+ JOIN rapport.projects p ON s.project_id = p.project_id
182
+ WHERE s.started_at > NOW() - INTERVAL '${lookbackDays} days'
183
+ AND s.ended_at IS NOT NULL
184
+ AND s.duration_seconds >= $1
185
+ AND NOT EXISTS (
186
+ SELECT 1 FROM rapport.session_correlations sc
187
+ WHERE sc.session_id = s.session_id
188
+ )
189
+ ${projectId ? 'AND s.project_id = $2' : ''}
190
+ ${companyId ? `AND p.company_id = ${projectId ? '$3' : '$2'}` : ''}
191
+ ORDER BY s.started_at ASC
192
+ LIMIT 500
193
+ `;
194
+
195
+ const params = [this.config.minSessionDuration];
196
+ if (projectId) params.push(projectId);
197
+ if (companyId) params.push(companyId);
198
+
199
+ const result = await executeQuery(query, params);
200
+ return result.rows;
201
+ }
202
+
203
+ /**
204
+ * Correlate a single session with commits
205
+ *
206
+ * @param {Object} session - Session to correlate
207
+ * @returns {Promise<Object>} Correlation result
208
+ */
209
+ async correlateSession(session) {
210
+ const correlation = {
211
+ sessionId: session.session_id,
212
+ projectId: session.project_id,
213
+ emailAddress: session.email_address,
214
+ sessionStarted: session.started_at,
215
+ sessionEnded: session.ended_at,
216
+ sessionDuration: session.duration_seconds,
217
+ hasCommits: false,
218
+ commitCount: 0,
219
+ commits: [],
220
+ totalInsertions: 0,
221
+ totalDeletions: 0,
222
+ totalFilesChanged: 0,
223
+ avgCommitLatencyMinutes: null,
224
+ patternsUsed: session.patterns_used || 0,
225
+ correlationScore: 0,
226
+ correlationType: 'none'
227
+ };
228
+
229
+ // Find commits within the correlation window
230
+ const commits = await this.findRelatedCommits(session);
231
+
232
+ if (commits.length === 0) {
233
+ return correlation;
234
+ }
235
+
236
+ correlation.hasCommits = true;
237
+ correlation.commitCount = commits.length;
238
+ correlation.commits = commits.map(c => c.commit_id);
239
+
240
+ // Calculate metrics
241
+ let totalLatency = 0;
242
+ for (const commit of commits) {
243
+ correlation.totalInsertions += commit.insertions || 0;
244
+ correlation.totalDeletions += commit.deletions || 0;
245
+ correlation.totalFilesChanged += commit.files_changed || 0;
246
+
247
+ // Calculate latency from session end to commit
248
+ const commitTime = new Date(commit.commit_timestamp);
249
+ const sessionEnd = new Date(session.ended_at);
250
+ const latencyMinutes = (commitTime - sessionEnd) / (1000 * 60);
251
+ totalLatency += Math.max(0, latencyMinutes);
252
+ }
253
+
254
+ correlation.avgCommitLatencyMinutes = commits.length > 0
255
+ ? Math.round(totalLatency / commits.length)
256
+ : null;
257
+
258
+ // Calculate correlation score
259
+ correlation.correlationScore = this.calculateCorrelationScore(session, commits);
260
+
261
+ // Determine correlation type
262
+ correlation.correlationType = this.determineCorrelationType(correlation);
263
+
264
+ return correlation;
265
+ }
266
+
267
+ /**
268
+ * Find commits related to a session
269
+ *
270
+ * @param {Object} session - Session to find commits for
271
+ * @returns {Promise<Array>} Related commits
272
+ */
273
+ async findRelatedCommits(session) {
274
+ const asyncWindowHours = this.config.asyncWindowHours;
275
+
276
+ // Find commits by the same author within the time window
277
+ const query = `
278
+ SELECT
279
+ c.commit_id,
280
+ c.commit_hash,
281
+ c.commit_timestamp,
282
+ c.insertions,
283
+ c.deletions,
284
+ c.files_changed,
285
+ c.branch,
286
+ c.message
287
+ FROM rapport.commits c
288
+ WHERE c.author_email = $1
289
+ AND c.commit_timestamp >= $2
290
+ AND c.commit_timestamp <= $3::timestamp + INTERVAL '${asyncWindowHours} hours'
291
+ ${session.repo_url ? `AND c.repo_url = $4` : ''}
292
+ ORDER BY c.commit_timestamp ASC
293
+ `;
294
+
295
+ const params = [
296
+ session.email_address,
297
+ session.started_at,
298
+ session.ended_at
299
+ ];
300
+ if (session.repo_url) params.push(session.repo_url);
301
+
302
+ const result = await executeQuery(query, params);
303
+ return result.rows;
304
+ }
305
+
306
+ /**
307
+ * Calculate correlation score based on various factors
308
+ *
309
+ * @param {Object} session - Session data
310
+ * @param {Array} commits - Related commits
311
+ * @returns {number} Correlation score (0.0 - 1.0)
312
+ */
313
+ calculateCorrelationScore(session, commits) {
314
+ if (commits.length === 0) return 0;
315
+
316
+ const weights = this.config.weights;
317
+ let score = 0;
318
+
319
+ // Time proximity score (closer = higher)
320
+ const sessionEnd = new Date(session.ended_at);
321
+ const avgLatency = commits.reduce((sum, c) => {
322
+ const commitTime = new Date(c.commit_timestamp);
323
+ return sum + Math.max(0, (commitTime - sessionEnd) / (1000 * 60 * 60)); // hours
324
+ }, 0) / commits.length;
325
+
326
+ // Exponential decay: closer commits get higher score
327
+ const timeScore = Math.exp(-avgLatency / this.config.correlationWindowHours);
328
+ score += timeScore * weights.timeProximity;
329
+
330
+ // Branch match score
331
+ if (session.git_branch) {
332
+ const branchMatches = commits.filter(c => c.branch === session.git_branch).length;
333
+ const branchScore = branchMatches / commits.length;
334
+ score += branchScore * weights.branchMatch;
335
+ } else {
336
+ // No branch info, give partial credit
337
+ score += 0.5 * weights.branchMatch;
338
+ }
339
+
340
+ // Project match (always 1.0 since we filter by repo_url)
341
+ score += 1.0 * weights.projectMatch;
342
+
343
+ // Pattern success (if patterns were used in session)
344
+ if (session.patterns_used > 0) {
345
+ score += 1.0 * weights.patternSuccess;
346
+ }
347
+
348
+ return Math.min(1.0, score);
349
+ }
350
+
351
+ /**
352
+ * Determine the type of correlation
353
+ *
354
+ * @param {Object} correlation - Correlation data
355
+ * @returns {string} Correlation type
356
+ */
357
+ determineCorrelationType(correlation) {
358
+ if (!correlation.hasCommits) {
359
+ return 'none';
360
+ }
361
+
362
+ // Immediate: commits within standard window
363
+ if (correlation.avgCommitLatencyMinutes <= this.config.correlationWindowHours * 60) {
364
+ return 'immediate';
365
+ }
366
+
367
+ // Async: commits within extended window
368
+ if (correlation.avgCommitLatencyMinutes <= this.config.asyncWindowHours * 60) {
369
+ return 'async';
370
+ }
371
+
372
+ return 'delayed';
373
+ }
374
+
375
+ /**
376
+ * Save correlation result to database
377
+ *
378
+ * @param {Object} correlation - Correlation data
379
+ */
380
+ async saveCorrelation(correlation) {
381
+ const query = `
382
+ INSERT INTO rapport.session_correlations (
383
+ session_id,
384
+ project_id,
385
+ email_address,
386
+ session_started_at,
387
+ session_ended_at,
388
+ session_duration_seconds,
389
+ has_commits,
390
+ commit_count,
391
+ commit_ids,
392
+ total_insertions,
393
+ total_deletions,
394
+ total_files_changed,
395
+ avg_commit_latency_minutes,
396
+ patterns_used,
397
+ correlation_score,
398
+ correlation_type,
399
+ analyzed_at
400
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, NOW())
401
+ ON CONFLICT (session_id) DO UPDATE SET
402
+ has_commits = EXCLUDED.has_commits,
403
+ commit_count = EXCLUDED.commit_count,
404
+ commit_ids = EXCLUDED.commit_ids,
405
+ total_insertions = EXCLUDED.total_insertions,
406
+ total_deletions = EXCLUDED.total_deletions,
407
+ total_files_changed = EXCLUDED.total_files_changed,
408
+ avg_commit_latency_minutes = EXCLUDED.avg_commit_latency_minutes,
409
+ correlation_score = EXCLUDED.correlation_score,
410
+ correlation_type = EXCLUDED.correlation_type,
411
+ analyzed_at = NOW()
412
+ `;
413
+
414
+ await executeQuery(query, [
415
+ correlation.sessionId,
416
+ correlation.projectId,
417
+ correlation.emailAddress,
418
+ correlation.sessionStarted,
419
+ correlation.sessionEnded,
420
+ correlation.sessionDuration,
421
+ correlation.hasCommits,
422
+ correlation.commitCount,
423
+ correlation.commits,
424
+ correlation.totalInsertions,
425
+ correlation.totalDeletions,
426
+ correlation.totalFilesChanged,
427
+ correlation.avgCommitLatencyMinutes,
428
+ correlation.patternsUsed,
429
+ correlation.correlationScore,
430
+ correlation.correlationType
431
+ ]);
432
+ }
433
+
434
+ /**
435
+ * Mark session as analyzed without commits
436
+ *
437
+ * @param {string} sessionId - Session ID
438
+ * @param {boolean} hasCommits - Whether commits were found
439
+ */
440
+ async markSessionAnalyzed(sessionId, hasCommits) {
441
+ const query = `
442
+ INSERT INTO rapport.session_correlations (
443
+ session_id,
444
+ has_commits,
445
+ correlation_type,
446
+ analyzed_at
447
+ )
448
+ SELECT
449
+ s.session_id,
450
+ $2,
451
+ 'none',
452
+ NOW()
453
+ FROM rapport.sessions s
454
+ WHERE s.session_id = $1
455
+ ON CONFLICT (session_id) DO UPDATE SET
456
+ analyzed_at = NOW()
457
+ `;
458
+
459
+ await executeQuery(query, [sessionId, hasCommits]);
460
+ }
461
+
462
+ /**
463
+ * Update pattern success rates based on correlation
464
+ *
465
+ * @param {Object} correlation - Correlation with commits
466
+ */
467
+ async updatePatternSuccess(correlation) {
468
+ // Get patterns used in the session
469
+ const patternsQuery = `
470
+ SELECT pattern_id
471
+ FROM rapport.pattern_usage
472
+ WHERE session_id = $1
473
+ `;
474
+
475
+ const result = await executeQuery(patternsQuery, [correlation.sessionId]);
476
+
477
+ for (const row of result.rows) {
478
+ // Record successful pattern usage (session led to commits)
479
+ await executeQuery(`
480
+ UPDATE rapport.patterns
481
+ SET
482
+ successful_handoffs = successful_handoffs + 1,
483
+ handoff_count = handoff_count + 1,
484
+ last_used = NOW()
485
+ WHERE pattern_id = $1
486
+ `, [row.pattern_id]);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Calculate aggregate metrics for dashboards
492
+ *
493
+ * @param {string} projectId - Optional project filter
494
+ * @param {string} companyId - Optional company filter
495
+ */
496
+ async calculateAggregateMetrics(projectId = null, companyId = null) {
497
+ // Refresh the developer activity view if it exists
498
+ try {
499
+ await executeQuery('SELECT rapport.refresh_developer_activity()');
500
+ } catch (error) {
501
+ // View might not exist, that's OK
502
+ console.log('[CorrelationAnalyzer] Could not refresh activity view:', error.message);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Get productivity metrics for a developer
508
+ *
509
+ * @param {string} emailAddress - Developer email
510
+ * @param {number} lookbackDays - Days to analyze
511
+ * @returns {Promise<Object>} Productivity metrics
512
+ */
513
+ async getDeveloperProductivity(emailAddress, lookbackDays = 30) {
514
+ const query = `
515
+ SELECT
516
+ COUNT(*) as total_sessions,
517
+ COUNT(*) FILTER (WHERE has_commits = true) as productive_sessions,
518
+ ROUND(
519
+ COUNT(*) FILTER (WHERE has_commits = true)::decimal /
520
+ NULLIF(COUNT(*), 0) * 100,
521
+ 1
522
+ ) as conversion_rate,
523
+ SUM(commit_count) as total_commits,
524
+ ROUND(AVG(commit_count) FILTER (WHERE has_commits = true), 1) as avg_commits_per_session,
525
+ SUM(total_insertions) as total_insertions,
526
+ SUM(total_deletions) as total_deletions,
527
+ SUM(total_files_changed) as total_files_changed,
528
+ ROUND(AVG(avg_commit_latency_minutes) FILTER (WHERE has_commits = true), 0) as avg_latency_minutes,
529
+ ROUND(AVG(correlation_score) FILTER (WHERE has_commits = true), 2) as avg_correlation_score,
530
+ SUM(session_duration_seconds)::integer as total_session_seconds,
531
+ COUNT(DISTINCT DATE(session_started_at)) as active_days
532
+ FROM rapport.session_correlations
533
+ WHERE email_address = $1
534
+ AND session_started_at > NOW() - INTERVAL '${lookbackDays} days'
535
+ `;
536
+
537
+ const result = await executeQuery(query, [emailAddress]);
538
+ const row = result.rows[0];
539
+
540
+ return {
541
+ email: emailAddress,
542
+ period: `${lookbackDays} days`,
543
+ totalSessions: parseInt(row.total_sessions) || 0,
544
+ productiveSessions: parseInt(row.productive_sessions) || 0,
545
+ conversionRate: parseFloat(row.conversion_rate) || 0,
546
+ totalCommits: parseInt(row.total_commits) || 0,
547
+ avgCommitsPerSession: parseFloat(row.avg_commits_per_session) || 0,
548
+ totalInsertions: parseInt(row.total_insertions) || 0,
549
+ totalDeletions: parseInt(row.total_deletions) || 0,
550
+ totalFilesChanged: parseInt(row.total_files_changed) || 0,
551
+ avgLatencyMinutes: parseInt(row.avg_latency_minutes) || 0,
552
+ avgCorrelationScore: parseFloat(row.avg_correlation_score) || 0,
553
+ totalSessionHours: Math.round((parseInt(row.total_session_seconds) || 0) / 3600 * 10) / 10,
554
+ activeDays: parseInt(row.active_days) || 0
555
+ };
556
+ }
557
+
558
+ /**
559
+ * Get project productivity metrics
560
+ *
561
+ * @param {string} projectId - Project ID
562
+ * @param {number} lookbackDays - Days to analyze
563
+ * @returns {Promise<Object>} Project metrics
564
+ */
565
+ async getProjectProductivity(projectId, lookbackDays = 30) {
566
+ const query = `
567
+ SELECT
568
+ COUNT(*) as total_sessions,
569
+ COUNT(*) FILTER (WHERE has_commits = true) as productive_sessions,
570
+ ROUND(
571
+ COUNT(*) FILTER (WHERE has_commits = true)::decimal /
572
+ NULLIF(COUNT(*), 0) * 100,
573
+ 1
574
+ ) as conversion_rate,
575
+ SUM(commit_count) as total_commits,
576
+ SUM(total_insertions) as total_insertions,
577
+ SUM(total_deletions) as total_deletions,
578
+ SUM(total_files_changed) as total_files_changed,
579
+ COUNT(DISTINCT email_address) as unique_developers,
580
+ ROUND(AVG(correlation_score) FILTER (WHERE has_commits = true), 2) as avg_correlation_score
581
+ FROM rapport.session_correlations
582
+ WHERE project_id = $1
583
+ AND session_started_at > NOW() - INTERVAL '${lookbackDays} days'
584
+ `;
585
+
586
+ const result = await executeQuery(query, [projectId]);
587
+ const row = result.rows[0];
588
+
589
+ return {
590
+ projectId,
591
+ period: `${lookbackDays} days`,
592
+ totalSessions: parseInt(row.total_sessions) || 0,
593
+ productiveSessions: parseInt(row.productive_sessions) || 0,
594
+ conversionRate: parseFloat(row.conversion_rate) || 0,
595
+ totalCommits: parseInt(row.total_commits) || 0,
596
+ totalInsertions: parseInt(row.total_insertions) || 0,
597
+ totalDeletions: parseInt(row.total_deletions) || 0,
598
+ totalFilesChanged: parseInt(row.total_files_changed) || 0,
599
+ uniqueDevelopers: parseInt(row.unique_developers) || 0,
600
+ avgCorrelationScore: parseFloat(row.avg_correlation_score) || 0
601
+ };
602
+ }
603
+
604
+ /**
605
+ * Get pattern effectiveness metrics
606
+ *
607
+ * @param {string} projectId - Optional project filter
608
+ * @param {number} lookbackDays - Days to analyze
609
+ * @returns {Promise<Array>} Pattern effectiveness data
610
+ */
611
+ async getPatternEffectiveness(projectId = null, lookbackDays = 30) {
612
+ const query = `
613
+ SELECT
614
+ p.pattern_id,
615
+ p.intent,
616
+ p.maturity,
617
+ COUNT(DISTINCT pu.session_id) as sessions_used,
618
+ COUNT(DISTINCT sc.session_id) FILTER (WHERE sc.has_commits = true) as sessions_with_commits,
619
+ ROUND(
620
+ COUNT(DISTINCT sc.session_id) FILTER (WHERE sc.has_commits = true)::decimal /
621
+ NULLIF(COUNT(DISTINCT pu.session_id), 0) * 100,
622
+ 1
623
+ ) as pattern_conversion_rate,
624
+ ROUND(AVG(sc.commit_count) FILTER (WHERE sc.has_commits = true), 1) as avg_commits
625
+ FROM rapport.patterns p
626
+ JOIN rapport.pattern_usage pu ON p.pattern_id = pu.pattern_id
627
+ LEFT JOIN rapport.session_correlations sc ON pu.session_id = sc.session_id
628
+ WHERE pu.used_at > NOW() - INTERVAL '${lookbackDays} days'
629
+ ${projectId ? 'AND p.project_id = $1' : ''}
630
+ GROUP BY p.pattern_id, p.intent, p.maturity
631
+ HAVING COUNT(DISTINCT pu.session_id) >= 3
632
+ ORDER BY pattern_conversion_rate DESC NULLS LAST
633
+ LIMIT 20
634
+ `;
635
+
636
+ const params = projectId ? [projectId] : [];
637
+ const result = await executeQuery(query, params);
638
+
639
+ return result.rows.map(row => ({
640
+ patternId: row.pattern_id,
641
+ intent: row.intent,
642
+ maturity: row.maturity,
643
+ sessionsUsed: parseInt(row.sessions_used) || 0,
644
+ sessionsWithCommits: parseInt(row.sessions_with_commits) || 0,
645
+ patternConversionRate: parseFloat(row.pattern_conversion_rate) || 0,
646
+ avgCommits: parseFloat(row.avg_commits) || 0
647
+ }));
648
+ }
649
+
650
+ /**
651
+ * Identify struggling developers (sessions without commits)
652
+ *
653
+ * @param {string} companyId - Company ID
654
+ * @param {number} lookbackDays - Days to analyze
655
+ * @returns {Promise<Array>} Struggling developer data
656
+ */
657
+ async identifyStrugglingDevelopers(companyId, lookbackDays = 30) {
658
+ const thresholds = this.config.thresholds;
659
+
660
+ const query = `
661
+ SELECT
662
+ sc.email_address,
663
+ u."User_Display_Name" as display_name,
664
+ COUNT(*) as total_sessions,
665
+ COUNT(*) FILTER (WHERE has_commits = false) as unproductive_sessions,
666
+ ROUND(
667
+ COUNT(*) FILTER (WHERE has_commits = false)::decimal /
668
+ NULLIF(COUNT(*), 0) * 100,
669
+ 1
670
+ ) as unproductive_rate,
671
+ MAX(sc.session_started_at) FILTER (WHERE has_commits = true) as last_productive_session,
672
+ ROUND(AVG(sc.session_duration_seconds) / 60, 0) as avg_session_minutes
673
+ FROM rapport.session_correlations sc
674
+ JOIN rapport.projects p ON sc.project_id = p.project_id
675
+ JOIN "Users" u ON sc.email_address = u."Email_Address"
676
+ WHERE p.company_id = $1
677
+ AND sc.session_started_at > NOW() - INTERVAL '${lookbackDays} days'
678
+ GROUP BY sc.email_address, u."User_Display_Name"
679
+ HAVING COUNT(*) >= $2
680
+ AND ROUND(
681
+ COUNT(*) FILTER (WHERE has_commits = false)::decimal /
682
+ NULLIF(COUNT(*), 0) * 100,
683
+ 1
684
+ ) >= (100 - $3 * 100)
685
+ ORDER BY unproductive_rate DESC
686
+ `;
687
+
688
+ const result = await executeQuery(query, [
689
+ companyId,
690
+ thresholds.minSessionsForStats,
691
+ thresholds.lowConversionRate
692
+ ]);
693
+
694
+ return result.rows.map(row => ({
695
+ email: row.email_address,
696
+ displayName: row.display_name,
697
+ totalSessions: parseInt(row.total_sessions),
698
+ unproductiveSessions: parseInt(row.unproductive_sessions),
699
+ unproductiveRate: parseFloat(row.unproductive_rate),
700
+ lastProductiveSession: row.last_productive_session,
701
+ avgSessionMinutes: parseInt(row.avg_session_minutes) || 0
702
+ }));
703
+ }
704
+
705
+ /**
706
+ * Get correlation summary for dashboard
707
+ *
708
+ * @param {string} companyId - Company ID
709
+ * @param {number} lookbackDays - Days to analyze
710
+ * @returns {Promise<Object>} Summary data
711
+ */
712
+ async getCorrelationSummary(companyId, lookbackDays = 30) {
713
+ const query = `
714
+ SELECT
715
+ COUNT(*) as total_sessions,
716
+ COUNT(*) FILTER (WHERE has_commits = true) as productive_sessions,
717
+ ROUND(
718
+ COUNT(*) FILTER (WHERE has_commits = true)::decimal /
719
+ NULLIF(COUNT(*), 0) * 100,
720
+ 1
721
+ ) as overall_conversion_rate,
722
+ SUM(commit_count) as total_commits,
723
+ COUNT(DISTINCT email_address) as unique_developers,
724
+ COUNT(DISTINCT sc.project_id) as active_projects,
725
+ SUM(total_insertions) as total_insertions,
726
+ SUM(total_deletions) as total_deletions,
727
+ SUM(total_files_changed) as total_files_changed,
728
+ ROUND(AVG(session_duration_seconds) / 60, 0) as avg_session_minutes,
729
+ ROUND(AVG(correlation_score) FILTER (WHERE has_commits = true), 2) as avg_correlation_score,
730
+ COUNT(*) FILTER (WHERE correlation_type = 'immediate') as immediate_correlations,
731
+ COUNT(*) FILTER (WHERE correlation_type = 'async') as async_correlations,
732
+ COUNT(*) FILTER (WHERE correlation_type = 'delayed') as delayed_correlations,
733
+ COUNT(*) FILTER (WHERE correlation_type = 'none') as no_correlations
734
+ FROM rapport.session_correlations sc
735
+ JOIN rapport.projects p ON sc.project_id = p.project_id
736
+ WHERE p.company_id = $1
737
+ AND sc.session_started_at > NOW() - INTERVAL '${lookbackDays} days'
738
+ `;
739
+
740
+ const result = await executeQuery(query, [companyId]);
741
+ const row = result.rows[0];
742
+
743
+ return {
744
+ companyId,
745
+ period: `${lookbackDays} days`,
746
+ totalSessions: parseInt(row.total_sessions) || 0,
747
+ productiveSessions: parseInt(row.productive_sessions) || 0,
748
+ overallConversionRate: parseFloat(row.overall_conversion_rate) || 0,
749
+ totalCommits: parseInt(row.total_commits) || 0,
750
+ uniqueDevelopers: parseInt(row.unique_developers) || 0,
751
+ activeProjects: parseInt(row.active_projects) || 0,
752
+ totalInsertions: parseInt(row.total_insertions) || 0,
753
+ totalDeletions: parseInt(row.total_deletions) || 0,
754
+ totalFilesChanged: parseInt(row.total_files_changed) || 0,
755
+ avgSessionMinutes: parseInt(row.avg_session_minutes) || 0,
756
+ avgCorrelationScore: parseFloat(row.avg_correlation_score) || 0,
757
+ correlationTypes: {
758
+ immediate: parseInt(row.immediate_correlations) || 0,
759
+ async: parseInt(row.async_correlations) || 0,
760
+ delayed: parseInt(row.delayed_correlations) || 0,
761
+ none: parseInt(row.no_correlations) || 0
762
+ }
763
+ };
764
+ }
765
+
766
+ /**
767
+ * Get configuration (for debugging/admin)
768
+ */
769
+ getConfiguration() {
770
+ return this.config;
771
+ }
772
+ }
773
+
774
+ module.exports = { CorrelationAnalyzer, DEFAULT_CONFIG };