@equilateral_ai/mindmeld 3.5.3 → 4.0.2

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 (138) hide show
  1. package/hooks/session-start.js +312 -85
  2. package/package.json +21 -13
  3. package/scripts/init-project.js +9 -23
  4. package/scripts/repo-analyzer.js +118 -2
  5. package/src/client/dbShim.js +16 -0
  6. package/src/core/AuthManager.js +3 -2
  7. package/src/handlers/helpers/dbOperations.js +9 -46
  8. package/src/index.js +2 -217
  9. package/src/utils/piiMask.js +16 -0
  10. package/scripts/inject.js +0 -409
  11. package/scripts/mcp-bridge.js +0 -220
  12. package/scripts/standards.js +0 -285
  13. package/src/collaboration/CollaborationPrompt.js +0 -460
  14. package/src/core/AlertEngine.js +0 -813
  15. package/src/core/AlertNotifier.js +0 -363
  16. package/src/core/CorrelationAnalyzer.js +0 -931
  17. package/src/core/CrossReferenceEngine.js +0 -624
  18. package/src/core/CurationEngine.js +0 -688
  19. package/src/core/DeprecationScheduler.js +0 -183
  20. package/src/core/LoadBearingDetector.js +0 -242
  21. package/src/core/NotificationService.js +0 -1032
  22. package/src/core/RapportOrchestrator.js +0 -632
  23. package/src/core/RelevanceDetector.js +0 -694
  24. package/src/core/StandardLifecycle.js +0 -244
  25. package/src/core/StandardsIngestion.js +0 -991
  26. package/src/core/TeamLoadBearingDetector.js +0 -431
  27. package/src/core/parsers/adrParser.js +0 -479
  28. package/src/core/parsers/cursorRulesParser.js +0 -564
  29. package/src/core/parsers/eslintParser.js +0 -439
  30. package/src/database/dbOperations.js +0 -105
  31. package/src/handlers/activity/activityGetMe.js +0 -98
  32. package/src/handlers/activity/activityGetTeam.js +0 -175
  33. package/src/handlers/admin/adminSetup.js +0 -216
  34. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  35. package/src/handlers/alerts/alertsGet.js +0 -250
  36. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  37. package/src/handlers/analytics/coachingGet.js +0 -361
  38. package/src/handlers/analytics/convergenceGet.js +0 -236
  39. package/src/handlers/analytics/developerScoreGet.js +0 -137
  40. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  41. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  42. package/src/handlers/collaborators/collaboratorList.js +0 -82
  43. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  44. package/src/handlers/collaborators/inviteAccept.js +0 -122
  45. package/src/handlers/company/companyUsersDelete.js +0 -141
  46. package/src/handlers/company/companyUsersGet.js +0 -90
  47. package/src/handlers/company/companyUsersPost.js +0 -267
  48. package/src/handlers/company/companyUsersPut.js +0 -76
  49. package/src/handlers/context/contextGet.js +0 -57
  50. package/src/handlers/context/invariantsGet.js +0 -74
  51. package/src/handlers/context/loopsGet.js +0 -82
  52. package/src/handlers/context/notesCreate.js +0 -74
  53. package/src/handlers/context/purposeGet.js +0 -78
  54. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  55. package/src/handlers/correlations/correlationsGet.js +0 -93
  56. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  57. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  58. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  59. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  60. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  61. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  62. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  63. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  64. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  65. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  66. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  67. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  68. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  69. package/src/handlers/github/githubConnectionStatus.js +0 -49
  70. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  71. package/src/handlers/github/githubOAuthCallback.js +0 -178
  72. package/src/handlers/github/githubOAuthStart.js +0 -59
  73. package/src/handlers/github/githubPatternsReview.js +0 -76
  74. package/src/handlers/github/githubReposList.js +0 -105
  75. package/src/handlers/health/healthGet.js +0 -55
  76. package/src/handlers/helpers/auditLogger.js +0 -201
  77. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  78. package/src/handlers/helpers/decisionFrames.js +0 -29
  79. package/src/handlers/helpers/errorHandler.js +0 -49
  80. package/src/handlers/helpers/index.js +0 -138
  81. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  82. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  83. package/src/handlers/helpers/predictiveCache.js +0 -51
  84. package/src/handlers/helpers/projectAccess.js +0 -88
  85. package/src/handlers/helpers/responseUtil.js +0 -55
  86. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  87. package/src/handlers/mcp/mcpHandler.js +0 -569
  88. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  89. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  90. package/src/handlers/notifications/getPreferences.js +0 -84
  91. package/src/handlers/notifications/sendNotification.js +0 -170
  92. package/src/handlers/notifications/updatePreferences.js +0 -316
  93. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  94. package/src/handlers/patterns/patternUsagePost.js +0 -182
  95. package/src/handlers/patterns/patternViolationPost.js +0 -185
  96. package/src/handlers/projects/projectCreate.js +0 -248
  97. package/src/handlers/projects/projectDelete.js +0 -82
  98. package/src/handlers/projects/projectGet.js +0 -95
  99. package/src/handlers/projects/projectUpdate.js +0 -117
  100. package/src/handlers/reports/aiLeverage.js +0 -210
  101. package/src/handlers/reports/engineeringInvestment.js +0 -132
  102. package/src/handlers/reports/riskForecast.js +0 -206
  103. package/src/handlers/reports/standardsRoi.js +0 -254
  104. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  105. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  106. package/src/handlers/scheduled/generateAlerts.js +0 -135
  107. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  108. package/src/handlers/scheduled/refreshActivity.js +0 -21
  109. package/src/handlers/scheduled/scanCompliance.js +0 -334
  110. package/src/handlers/sessions/sessionEndPost.js +0 -180
  111. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  112. package/src/handlers/standards/catalogGet.js +0 -185
  113. package/src/handlers/standards/catalogSync.js +0 -120
  114. package/src/handlers/standards/discoveriesGet.js +0 -89
  115. package/src/handlers/standards/projectStandardsGet.js +0 -129
  116. package/src/handlers/standards/projectStandardsPut.js +0 -151
  117. package/src/handlers/standards/standardsAuditGet.js +0 -65
  118. package/src/handlers/standards/standardsParseUpload.js +0 -149
  119. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  120. package/src/handlers/standards/standardsTransition.js +0 -161
  121. package/src/handlers/stripe/addonManagePost.js +0 -240
  122. package/src/handlers/stripe/billingPortalPost.js +0 -93
  123. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  124. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  125. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  126. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  127. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  128. package/src/handlers/stripe/webhookPost.js +0 -482
  129. package/src/handlers/user/apiTokenCreate.js +0 -71
  130. package/src/handlers/user/apiTokenList.js +0 -64
  131. package/src/handlers/user/userSplashAck.js +0 -91
  132. package/src/handlers/user/userSplashGet.js +0 -211
  133. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  134. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  135. package/src/handlers/users/userEntitlementsGet.js +0 -89
  136. package/src/handlers/users/userGet.js +0 -118
  137. package/src/handlers/users/userProfilePut.js +0 -77
  138. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,931 +0,0 @@
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
- // Update standards_patterns.correlation with observed effectiveness
148
- try {
149
- const updated = await this.updateStandardsCorrelation();
150
- summary.standardsCorrelationUpdated = updated;
151
- if (updated > 0) {
152
- console.log(`[CorrelationAnalyzer] Updated correlation scores for ${updated} standards`);
153
- }
154
- } catch (error) {
155
- console.error('[CorrelationAnalyzer] Could not update standards correlations:', error.message);
156
- summary.errors.push({ step: 'updateStandardsCorrelation', error: error.message });
157
- }
158
-
159
- summary.completedAt = new Date().toISOString();
160
- console.log(`[CorrelationAnalyzer] Analysis complete: ${summary.correlationsCreated} correlations, ${summary.uncorrelatedSessions} uncorrelated`);
161
-
162
- } catch (error) {
163
- console.error('[CorrelationAnalyzer] Error in correlation analysis:', error);
164
- summary.errors.push(error.message);
165
- }
166
-
167
- return summary;
168
- }
169
-
170
- /**
171
- * Get sessions that haven't been correlated yet
172
- *
173
- * @param {number} lookbackDays - Days to look back
174
- * @param {string} projectId - Optional project filter
175
- * @param {string} companyId - Optional company filter
176
- * @returns {Promise<Array>} Uncorrelated sessions
177
- */
178
- async getUncorrelatedSessions(lookbackDays, projectId = null, companyId = null) {
179
- const query = `
180
- SELECT
181
- s.session_id,
182
- s.project_id,
183
- s.email_address,
184
- s.started_at,
185
- s.ended_at,
186
- s.duration_seconds,
187
- s.git_branch,
188
- s.patterns_used,
189
- s.session_data,
190
- p.company_id,
191
- p.repo_url
192
- FROM rapport.sessions s
193
- JOIN rapport.projects p ON s.project_id = p.project_id
194
- WHERE s.started_at > NOW() - INTERVAL '${lookbackDays} days'
195
- AND s.ended_at IS NOT NULL
196
- AND s.duration_seconds >= $1
197
- AND NOT EXISTS (
198
- SELECT 1 FROM rapport.session_correlations sc
199
- WHERE sc.session_id = s.session_id
200
- )
201
- ${projectId ? 'AND s.project_id = $2' : ''}
202
- ${companyId ? `AND p.company_id = ${projectId ? '$3' : '$2'}` : ''}
203
- ORDER BY s.started_at ASC
204
- LIMIT 500
205
- `;
206
-
207
- const params = [this.config.minSessionDuration];
208
- if (projectId) params.push(projectId);
209
- if (companyId) params.push(companyId);
210
-
211
- const result = await executeQuery(query, params);
212
- return result.rows;
213
- }
214
-
215
- /**
216
- * Correlate a single session with commits
217
- *
218
- * @param {Object} session - Session to correlate
219
- * @returns {Promise<Object>} Correlation result
220
- */
221
- async correlateSession(session) {
222
- const correlation = {
223
- sessionId: session.session_id,
224
- projectId: session.project_id,
225
- emailAddress: session.email_address,
226
- sessionStarted: session.started_at,
227
- sessionEnded: session.ended_at,
228
- sessionDuration: session.duration_seconds,
229
- hasCommits: false,
230
- commitCount: 0,
231
- commits: [],
232
- totalInsertions: 0,
233
- totalDeletions: 0,
234
- totalFilesChanged: 0,
235
- avgCommitLatencyMinutes: null,
236
- patternsUsed: session.patterns_used || 0,
237
- correlationScore: 0,
238
- correlationType: 'none'
239
- };
240
-
241
- // Find commits within the correlation window
242
- const commits = await this.findRelatedCommits(session);
243
-
244
- if (commits.length === 0) {
245
- return correlation;
246
- }
247
-
248
- correlation.hasCommits = true;
249
- correlation.commitCount = commits.length;
250
- correlation.commits = commits.map(c => c.commit_id);
251
-
252
- // Calculate metrics
253
- let totalLatency = 0;
254
- for (const commit of commits) {
255
- correlation.totalInsertions += commit.insertions || 0;
256
- correlation.totalDeletions += commit.deletions || 0;
257
- correlation.totalFilesChanged += commit.files_changed || 0;
258
-
259
- // Calculate latency from session end to commit
260
- const commitTime = new Date(commit.commit_timestamp);
261
- const sessionEnd = new Date(session.ended_at);
262
- const latencyMinutes = (commitTime - sessionEnd) / (1000 * 60);
263
- totalLatency += Math.max(0, latencyMinutes);
264
- }
265
-
266
- correlation.avgCommitLatencyMinutes = commits.length > 0
267
- ? Math.round(totalLatency / commits.length)
268
- : null;
269
-
270
- // Calculate correlation score
271
- correlation.correlationScore = this.calculateCorrelationScore(session, commits);
272
-
273
- // Determine correlation type
274
- correlation.correlationType = this.determineCorrelationType(correlation);
275
-
276
- return correlation;
277
- }
278
-
279
- /**
280
- * Find commits related to a session
281
- *
282
- * @param {Object} session - Session to find commits for
283
- * @returns {Promise<Array>} Related commits
284
- */
285
- async findRelatedCommits(session) {
286
- const asyncWindowHours = this.config.asyncWindowHours;
287
-
288
- // Find commits by the same author within the time window
289
- const query = `
290
- SELECT
291
- c.commit_id,
292
- c.commit_hash,
293
- c.commit_timestamp,
294
- c.insertions,
295
- c.deletions,
296
- c.files_changed,
297
- c.branch,
298
- c.message
299
- FROM rapport.commits c
300
- WHERE c.author_email = $1
301
- AND c.commit_timestamp >= $2
302
- AND c.commit_timestamp <= $3::timestamp + INTERVAL '${asyncWindowHours} hours'
303
- ${session.repo_url ? `AND c.repo_url = $4` : ''}
304
- ORDER BY c.commit_timestamp ASC
305
- `;
306
-
307
- const params = [
308
- session.email_address,
309
- session.started_at,
310
- session.ended_at
311
- ];
312
- if (session.repo_url) params.push(session.repo_url);
313
-
314
- const result = await executeQuery(query, params);
315
- return result.rows;
316
- }
317
-
318
- /**
319
- * Calculate correlation score based on various factors
320
- *
321
- * @param {Object} session - Session data
322
- * @param {Array} commits - Related commits
323
- * @returns {number} Correlation score (0.0 - 1.0)
324
- */
325
- calculateCorrelationScore(session, commits) {
326
- if (commits.length === 0) return 0;
327
-
328
- const weights = this.config.weights;
329
- let score = 0;
330
-
331
- // Time proximity score (closer = higher)
332
- const sessionEnd = new Date(session.ended_at);
333
- const avgLatency = commits.reduce((sum, c) => {
334
- const commitTime = new Date(c.commit_timestamp);
335
- return sum + Math.max(0, (commitTime - sessionEnd) / (1000 * 60 * 60)); // hours
336
- }, 0) / commits.length;
337
-
338
- // Exponential decay: closer commits get higher score
339
- const timeScore = Math.exp(-avgLatency / this.config.correlationWindowHours);
340
- score += timeScore * weights.timeProximity;
341
-
342
- // Branch match score
343
- if (session.git_branch) {
344
- const branchMatches = commits.filter(c => c.branch === session.git_branch).length;
345
- const branchScore = branchMatches / commits.length;
346
- score += branchScore * weights.branchMatch;
347
- } else {
348
- // No branch info, give partial credit
349
- score += 0.5 * weights.branchMatch;
350
- }
351
-
352
- // Project match (always 1.0 since we filter by repo_url)
353
- score += 1.0 * weights.projectMatch;
354
-
355
- // Pattern success (if patterns were used in session)
356
- if (session.patterns_used > 0) {
357
- score += 1.0 * weights.patternSuccess;
358
- }
359
-
360
- return Math.min(1.0, score);
361
- }
362
-
363
- /**
364
- * Determine the type of correlation
365
- *
366
- * @param {Object} correlation - Correlation data
367
- * @returns {string} Correlation type
368
- */
369
- determineCorrelationType(correlation) {
370
- if (!correlation.hasCommits) {
371
- return 'none';
372
- }
373
-
374
- // Immediate: commits within standard window
375
- if (correlation.avgCommitLatencyMinutes <= this.config.correlationWindowHours * 60) {
376
- return 'immediate';
377
- }
378
-
379
- // Async: commits within extended window
380
- if (correlation.avgCommitLatencyMinutes <= this.config.asyncWindowHours * 60) {
381
- return 'async';
382
- }
383
-
384
- return 'delayed';
385
- }
386
-
387
- /**
388
- * Save correlation result to database
389
- *
390
- * @param {Object} correlation - Correlation data
391
- */
392
- async saveCorrelation(correlation) {
393
- const query = `
394
- INSERT INTO rapport.session_correlations (
395
- session_id,
396
- project_id,
397
- email_address,
398
- session_started_at,
399
- session_ended_at,
400
- session_duration_seconds,
401
- has_commits,
402
- commit_count,
403
- commit_ids,
404
- total_insertions,
405
- total_deletions,
406
- total_files_changed,
407
- avg_commit_latency_minutes,
408
- patterns_used,
409
- correlation_score,
410
- correlation_type,
411
- analyzed_at
412
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, NOW())
413
- ON CONFLICT (session_id) DO UPDATE SET
414
- has_commits = EXCLUDED.has_commits,
415
- commit_count = EXCLUDED.commit_count,
416
- commit_ids = EXCLUDED.commit_ids,
417
- total_insertions = EXCLUDED.total_insertions,
418
- total_deletions = EXCLUDED.total_deletions,
419
- total_files_changed = EXCLUDED.total_files_changed,
420
- avg_commit_latency_minutes = EXCLUDED.avg_commit_latency_minutes,
421
- correlation_score = EXCLUDED.correlation_score,
422
- correlation_type = EXCLUDED.correlation_type,
423
- analyzed_at = NOW()
424
- `;
425
-
426
- await executeQuery(query, [
427
- correlation.sessionId,
428
- correlation.projectId,
429
- correlation.emailAddress,
430
- correlation.sessionStarted,
431
- correlation.sessionEnded,
432
- correlation.sessionDuration,
433
- correlation.hasCommits,
434
- correlation.commitCount,
435
- correlation.commits,
436
- correlation.totalInsertions,
437
- correlation.totalDeletions,
438
- correlation.totalFilesChanged,
439
- correlation.avgCommitLatencyMinutes,
440
- correlation.patternsUsed,
441
- correlation.correlationScore,
442
- correlation.correlationType
443
- ]);
444
- }
445
-
446
- /**
447
- * Mark session as analyzed without commits
448
- *
449
- * @param {string} sessionId - Session ID
450
- * @param {boolean} hasCommits - Whether commits were found
451
- */
452
- async markSessionAnalyzed(sessionId, hasCommits) {
453
- const query = `
454
- INSERT INTO rapport.session_correlations (
455
- session_id,
456
- has_commits,
457
- correlation_type,
458
- analyzed_at
459
- )
460
- SELECT
461
- s.session_id,
462
- $2,
463
- 'none',
464
- NOW()
465
- FROM rapport.sessions s
466
- WHERE s.session_id = $1
467
- ON CONFLICT (session_id) DO UPDATE SET
468
- analyzed_at = NOW()
469
- `;
470
-
471
- await executeQuery(query, [sessionId, hasCommits]);
472
- }
473
-
474
- /**
475
- * Update pattern success rates based on correlation
476
- *
477
- * @param {Object} correlation - Correlation with commits
478
- */
479
- async updatePatternSuccess(correlation) {
480
- // Get patterns used in the session
481
- const patternsQuery = `
482
- SELECT pattern_id
483
- FROM rapport.pattern_usage
484
- WHERE session_id = $1
485
- `;
486
-
487
- const result = await executeQuery(patternsQuery, [correlation.sessionId]);
488
-
489
- for (const row of result.rows) {
490
- // Record successful pattern usage (session led to commits)
491
- await executeQuery(`
492
- UPDATE rapport.patterns
493
- SET
494
- successful_handoffs = successful_handoffs + 1,
495
- handoff_count = handoff_count + 1,
496
- last_used = NOW()
497
- WHERE pattern_id = $1
498
- `, [row.pattern_id]);
499
- }
500
- }
501
-
502
- /**
503
- * Calculate aggregate metrics for dashboards
504
- *
505
- * @param {string} projectId - Optional project filter
506
- * @param {string} companyId - Optional company filter
507
- */
508
- async calculateAggregateMetrics(projectId = null, companyId = null) {
509
- // Refresh the developer activity view if it exists
510
- try {
511
- await executeQuery('SELECT rapport.refresh_developer_activity()');
512
- } catch (error) {
513
- // View might not exist, that's OK
514
- console.log('[CorrelationAnalyzer] Could not refresh activity view:', error.message);
515
- }
516
- }
517
-
518
- /**
519
- * Get productivity metrics for a developer
520
- *
521
- * @param {string} emailAddress - Developer email
522
- * @param {number} lookbackDays - Days to analyze
523
- * @returns {Promise<Object>} Productivity metrics
524
- */
525
- async getDeveloperProductivity(emailAddress, lookbackDays = 30) {
526
- const query = `
527
- SELECT
528
- COUNT(*) as total_sessions,
529
- COUNT(*) FILTER (WHERE has_commits = true) as productive_sessions,
530
- ROUND(
531
- COUNT(*) FILTER (WHERE has_commits = true)::decimal /
532
- NULLIF(COUNT(*), 0) * 100,
533
- 1
534
- ) as conversion_rate,
535
- SUM(commit_count) as total_commits,
536
- ROUND(AVG(commit_count) FILTER (WHERE has_commits = true), 1) as avg_commits_per_session,
537
- SUM(total_insertions) as total_insertions,
538
- SUM(total_deletions) as total_deletions,
539
- SUM(total_files_changed) as total_files_changed,
540
- ROUND(AVG(avg_commit_latency_minutes) FILTER (WHERE has_commits = true), 0) as avg_latency_minutes,
541
- ROUND(AVG(correlation_score) FILTER (WHERE has_commits = true), 2) as avg_correlation_score,
542
- SUM(session_duration_seconds)::integer as total_session_seconds,
543
- COUNT(DISTINCT DATE(session_started_at)) as active_days
544
- FROM rapport.session_correlations
545
- WHERE email_address = $1
546
- AND session_started_at > NOW() - INTERVAL '${lookbackDays} days'
547
- `;
548
-
549
- const result = await executeQuery(query, [emailAddress]);
550
- const row = result.rows[0];
551
-
552
- return {
553
- email: emailAddress,
554
- period: `${lookbackDays} days`,
555
- totalSessions: parseInt(row.total_sessions) || 0,
556
- productiveSessions: parseInt(row.productive_sessions) || 0,
557
- conversionRate: parseFloat(row.conversion_rate) || 0,
558
- totalCommits: parseInt(row.total_commits) || 0,
559
- avgCommitsPerSession: parseFloat(row.avg_commits_per_session) || 0,
560
- totalInsertions: parseInt(row.total_insertions) || 0,
561
- totalDeletions: parseInt(row.total_deletions) || 0,
562
- totalFilesChanged: parseInt(row.total_files_changed) || 0,
563
- avgLatencyMinutes: parseInt(row.avg_latency_minutes) || 0,
564
- avgCorrelationScore: parseFloat(row.avg_correlation_score) || 0,
565
- totalSessionHours: Math.round((parseInt(row.total_session_seconds) || 0) / 3600 * 10) / 10,
566
- activeDays: parseInt(row.active_days) || 0
567
- };
568
- }
569
-
570
- /**
571
- * Get project productivity metrics
572
- *
573
- * @param {string} projectId - Project ID
574
- * @param {number} lookbackDays - Days to analyze
575
- * @returns {Promise<Object>} Project metrics
576
- */
577
- async getProjectProductivity(projectId, lookbackDays = 30) {
578
- const query = `
579
- SELECT
580
- COUNT(*) as total_sessions,
581
- COUNT(*) FILTER (WHERE has_commits = true) as productive_sessions,
582
- ROUND(
583
- COUNT(*) FILTER (WHERE has_commits = true)::decimal /
584
- NULLIF(COUNT(*), 0) * 100,
585
- 1
586
- ) as conversion_rate,
587
- SUM(commit_count) as total_commits,
588
- SUM(total_insertions) as total_insertions,
589
- SUM(total_deletions) as total_deletions,
590
- SUM(total_files_changed) as total_files_changed,
591
- COUNT(DISTINCT email_address) as unique_developers,
592
- ROUND(AVG(correlation_score) FILTER (WHERE has_commits = true), 2) as avg_correlation_score
593
- FROM rapport.session_correlations
594
- WHERE project_id = $1
595
- AND session_started_at > NOW() - INTERVAL '${lookbackDays} days'
596
- `;
597
-
598
- const result = await executeQuery(query, [projectId]);
599
- const row = result.rows[0];
600
-
601
- return {
602
- projectId,
603
- period: `${lookbackDays} days`,
604
- totalSessions: parseInt(row.total_sessions) || 0,
605
- productiveSessions: parseInt(row.productive_sessions) || 0,
606
- conversionRate: parseFloat(row.conversion_rate) || 0,
607
- totalCommits: parseInt(row.total_commits) || 0,
608
- totalInsertions: parseInt(row.total_insertions) || 0,
609
- totalDeletions: parseInt(row.total_deletions) || 0,
610
- totalFilesChanged: parseInt(row.total_files_changed) || 0,
611
- uniqueDevelopers: parseInt(row.unique_developers) || 0,
612
- avgCorrelationScore: parseFloat(row.avg_correlation_score) || 0
613
- };
614
- }
615
-
616
- /**
617
- * Get pattern effectiveness metrics
618
- *
619
- * @param {string} projectId - Optional project filter
620
- * @param {number} lookbackDays - Days to analyze
621
- * @returns {Promise<Array>} Pattern effectiveness data
622
- */
623
- async getPatternEffectiveness(projectId = null, lookbackDays = 30) {
624
- const query = `
625
- SELECT
626
- p.pattern_id,
627
- p.intent,
628
- p.maturity,
629
- COUNT(DISTINCT pu.session_id) as sessions_used,
630
- COUNT(DISTINCT sc.session_id) FILTER (WHERE sc.has_commits = true) as sessions_with_commits,
631
- ROUND(
632
- COUNT(DISTINCT sc.session_id) FILTER (WHERE sc.has_commits = true)::decimal /
633
- NULLIF(COUNT(DISTINCT pu.session_id), 0) * 100,
634
- 1
635
- ) as pattern_conversion_rate,
636
- ROUND(AVG(sc.commit_count) FILTER (WHERE sc.has_commits = true), 1) as avg_commits
637
- FROM rapport.patterns p
638
- JOIN rapport.pattern_usage pu ON p.pattern_id = pu.pattern_id
639
- LEFT JOIN rapport.session_correlations sc ON pu.session_id = sc.session_id
640
- WHERE pu.used_at > NOW() - INTERVAL '${lookbackDays} days'
641
- ${projectId ? 'AND p.project_id = $1' : ''}
642
- GROUP BY p.pattern_id, p.intent, p.maturity
643
- HAVING COUNT(DISTINCT pu.session_id) >= 3
644
- ORDER BY pattern_conversion_rate DESC NULLS LAST
645
- LIMIT 20
646
- `;
647
-
648
- const params = projectId ? [projectId] : [];
649
- const result = await executeQuery(query, params);
650
-
651
- return result.rows.map(row => ({
652
- patternId: row.pattern_id,
653
- intent: row.intent,
654
- maturity: row.maturity,
655
- sessionsUsed: parseInt(row.sessions_used) || 0,
656
- sessionsWithCommits: parseInt(row.sessions_with_commits) || 0,
657
- patternConversionRate: parseFloat(row.pattern_conversion_rate) || 0,
658
- avgCommits: parseFloat(row.avg_commits) || 0
659
- }));
660
- }
661
-
662
- /**
663
- * Identify struggling developers (sessions without commits)
664
- *
665
- * @param {string} companyId - Company ID
666
- * @param {number} lookbackDays - Days to analyze
667
- * @returns {Promise<Array>} Struggling developer data
668
- */
669
- async identifyStrugglingDevelopers(companyId, lookbackDays = 30) {
670
- const thresholds = this.config.thresholds;
671
-
672
- const query = `
673
- SELECT
674
- sc.email_address,
675
- u."User_Display_Name" as display_name,
676
- COUNT(*) as total_sessions,
677
- COUNT(*) FILTER (WHERE has_commits = false) as unproductive_sessions,
678
- ROUND(
679
- COUNT(*) FILTER (WHERE has_commits = false)::decimal /
680
- NULLIF(COUNT(*), 0) * 100,
681
- 1
682
- ) as unproductive_rate,
683
- MAX(sc.session_started_at) FILTER (WHERE has_commits = true) as last_productive_session,
684
- ROUND(AVG(sc.session_duration_seconds) / 60, 0) as avg_session_minutes
685
- FROM rapport.session_correlations sc
686
- JOIN rapport.projects p ON sc.project_id = p.project_id
687
- JOIN "Users" u ON sc.email_address = u."Email_Address"
688
- WHERE p.company_id = $1
689
- AND sc.session_started_at > NOW() - INTERVAL '${lookbackDays} days'
690
- GROUP BY sc.email_address, u."User_Display_Name"
691
- HAVING COUNT(*) >= $2
692
- AND ROUND(
693
- COUNT(*) FILTER (WHERE has_commits = false)::decimal /
694
- NULLIF(COUNT(*), 0) * 100,
695
- 1
696
- ) >= (100 - $3 * 100)
697
- ORDER BY unproductive_rate DESC
698
- `;
699
-
700
- const result = await executeQuery(query, [
701
- companyId,
702
- thresholds.minSessionsForStats,
703
- thresholds.lowConversionRate
704
- ]);
705
-
706
- return result.rows.map(row => ({
707
- email: row.email_address,
708
- displayName: row.display_name,
709
- totalSessions: parseInt(row.total_sessions),
710
- unproductiveSessions: parseInt(row.unproductive_sessions),
711
- unproductiveRate: parseFloat(row.unproductive_rate),
712
- lastProductiveSession: row.last_productive_session,
713
- avgSessionMinutes: parseInt(row.avg_session_minutes) || 0
714
- }));
715
- }
716
-
717
- /**
718
- * Get correlation summary for dashboard
719
- *
720
- * @param {string} companyId - Company ID
721
- * @param {number} lookbackDays - Days to analyze
722
- * @returns {Promise<Object>} Summary data
723
- */
724
- async getCorrelationSummary(companyId, lookbackDays = 30) {
725
- const query = `
726
- SELECT
727
- COUNT(*) as total_sessions,
728
- COUNT(*) FILTER (WHERE has_commits = true) as productive_sessions,
729
- ROUND(
730
- COUNT(*) FILTER (WHERE has_commits = true)::decimal /
731
- NULLIF(COUNT(*), 0) * 100,
732
- 1
733
- ) as overall_conversion_rate,
734
- SUM(commit_count) as total_commits,
735
- COUNT(DISTINCT email_address) as unique_developers,
736
- COUNT(DISTINCT sc.project_id) as active_projects,
737
- SUM(total_insertions) as total_insertions,
738
- SUM(total_deletions) as total_deletions,
739
- SUM(total_files_changed) as total_files_changed,
740
- ROUND(AVG(session_duration_seconds) / 60, 0) as avg_session_minutes,
741
- ROUND(AVG(correlation_score) FILTER (WHERE has_commits = true), 2) as avg_correlation_score,
742
- COUNT(*) FILTER (WHERE correlation_type = 'immediate') as immediate_correlations,
743
- COUNT(*) FILTER (WHERE correlation_type = 'async') as async_correlations,
744
- COUNT(*) FILTER (WHERE correlation_type = 'delayed') as delayed_correlations,
745
- COUNT(*) FILTER (WHERE correlation_type = 'none') as no_correlations
746
- FROM rapport.session_correlations sc
747
- JOIN rapport.projects p ON sc.project_id = p.project_id
748
- WHERE p.company_id = $1
749
- AND sc.session_started_at > NOW() - INTERVAL '${lookbackDays} days'
750
- `;
751
-
752
- const result = await executeQuery(query, [companyId]);
753
- const row = result.rows[0];
754
-
755
- return {
756
- companyId,
757
- period: `${lookbackDays} days`,
758
- totalSessions: parseInt(row.total_sessions) || 0,
759
- productiveSessions: parseInt(row.productive_sessions) || 0,
760
- overallConversionRate: parseFloat(row.overall_conversion_rate) || 0,
761
- totalCommits: parseInt(row.total_commits) || 0,
762
- uniqueDevelopers: parseInt(row.unique_developers) || 0,
763
- activeProjects: parseInt(row.active_projects) || 0,
764
- totalInsertions: parseInt(row.total_insertions) || 0,
765
- totalDeletions: parseInt(row.total_deletions) || 0,
766
- totalFilesChanged: parseInt(row.total_files_changed) || 0,
767
- avgSessionMinutes: parseInt(row.avg_session_minutes) || 0,
768
- avgCorrelationScore: parseFloat(row.avg_correlation_score) || 0,
769
- correlationTypes: {
770
- immediate: parseInt(row.immediate_correlations) || 0,
771
- async: parseInt(row.async_correlations) || 0,
772
- delayed: parseInt(row.delayed_correlations) || 0,
773
- none: parseInt(row.no_correlations) || 0
774
- }
775
- };
776
- }
777
-
778
- /**
779
- * Update standards_patterns.correlation with observed effectiveness
780
- *
781
- * Computes per-standard effectiveness by joining session_standards
782
- * (which standards were shown) with session_correlations (which
783
- * sessions produced commits). Standards shown in sessions that
784
- * produce commits get higher correlation scores.
785
- *
786
- * This directly influences relevance scoring since
787
- * standardsRelevantPost.js uses correlation * 40 as the base score.
788
- *
789
- * @returns {Promise<number>} Number of standards updated
790
- */
791
- async updateStandardsCorrelation() {
792
- const query = `
793
- UPDATE rapport.standards_patterns sp SET
794
- correlation = LEAST(sub.effectiveness, 1.00),
795
- last_updated = NOW()
796
- FROM (
797
- SELECT ss.standard_id,
798
- COUNT(*) FILTER (WHERE sc.has_commits = true)::decimal / COUNT(*) as effectiveness
799
- FROM rapport.session_standards ss
800
- JOIN rapport.session_correlations sc ON sc.session_id = ss.session_id
801
- WHERE sc.analyzed_at > NOW() - INTERVAL '30 days'
802
- GROUP BY ss.standard_id
803
- HAVING COUNT(*) >= 5
804
- ) sub
805
- WHERE sp.pattern_id = sub.standard_id
806
- `;
807
-
808
- const result = await executeQuery(query);
809
- return result.rowCount || 0;
810
- }
811
-
812
- // ─── Agent Execution Correlation ──────────────────────────────
813
-
814
- /**
815
- * Analyze correlations for agent execution patterns.
816
- * Instead of session → git commit, this correlates:
817
- * - Agent decision → task outcome success rate
818
- * - Recovery effectiveness (did failover agents succeed?)
819
- * - Tier accuracy (tier matches actual consequence?)
820
- * - Cross-workflow pattern reuse
821
- *
822
- * @param {Object} options - Analysis options
823
- * @param {string} options.projectId - Project filter
824
- * @param {number} options.lookbackDays - Days to analyze (default: 30)
825
- * @returns {Promise<Object>} Agent execution correlation summary
826
- */
827
- async analyzeAgentExecutionCorrelations(options = {}) {
828
- const lookbackDays = options.lookbackDays || 30;
829
-
830
- const summary = {
831
- startedAt: new Date().toISOString(),
832
- agentsAnalyzed: 0,
833
- correlationsCreated: 0,
834
- patterns: [],
835
- errors: []
836
- };
837
-
838
- try {
839
- // Get agent execution patterns from pattern_usage
840
- const agentPatterns = await executeQuery(`
841
- SELECT
842
- p.element,
843
- p.category,
844
- COUNT(pu.*) AS total_uses,
845
- COUNT(pu.*) FILTER (WHERE pu.success = true) AS successful_uses,
846
- COUNT(DISTINCT pu.email_address) AS unique_agents,
847
- AVG(CASE WHEN pu.success THEN 1.0 ELSE 0.0 END) AS success_rate,
848
- MAX(pu.used_at) AS last_used
849
- FROM rapport.patterns p
850
- JOIN rapport.pattern_usage pu ON pu.pattern_id = p.pattern_id
851
- WHERE p.pattern_data->>'source' = 'agent_execution'
852
- AND pu.used_at > NOW() - INTERVAL '${lookbackDays} days'
853
- ${options.projectId ? `AND p.project_id = '${options.projectId}'` : ''}
854
- GROUP BY p.element, p.category
855
- HAVING COUNT(pu.*) >= 3
856
- ORDER BY success_rate ASC
857
- `);
858
-
859
- if (agentPatterns?.rows) {
860
- for (const pattern of agentPatterns.rows) {
861
- summary.agentsAnalyzed++;
862
- summary.patterns.push({
863
- element: pattern.element,
864
- category: pattern.category,
865
- totalUses: parseInt(pattern.total_uses),
866
- successRate: parseFloat(pattern.success_rate),
867
- uniqueAgents: parseInt(pattern.unique_agents),
868
- lastUsed: pattern.last_used
869
- });
870
- }
871
- }
872
-
873
- // Update pattern correlations based on agent execution data
874
- const updated = await this.updateAgentPatternCorrelations();
875
- summary.correlationsCreated = updated;
876
-
877
- } catch (error) {
878
- summary.errors.push(error.message);
879
- }
880
-
881
- summary.completedAt = new Date().toISOString();
882
- return summary;
883
- }
884
-
885
- /**
886
- * Update pattern correlations for agent execution patterns.
887
- * Uses agent task success as the "proof" (instead of git commits for code sessions).
888
- *
889
- * @returns {Promise<number>} Number of patterns updated
890
- */
891
- async updateAgentPatternCorrelations() {
892
- const query = `
893
- UPDATE rapport.patterns p SET
894
- handoff_count = sub.total_uses,
895
- successful_handoffs = sub.successful_uses,
896
- failed_handoffs = sub.failed_uses,
897
- last_used = sub.last_used
898
- FROM (
899
- SELECT
900
- pu.pattern_id,
901
- COUNT(*) AS total_uses,
902
- COUNT(*) FILTER (WHERE pu.success = true) AS successful_uses,
903
- COUNT(*) FILTER (WHERE pu.success = false) AS failed_uses,
904
- MAX(pu.used_at) AS last_used
905
- FROM rapport.pattern_usage pu
906
- JOIN rapport.patterns pat ON pat.pattern_id = pu.pattern_id
907
- WHERE pat.pattern_data->>'source' = 'agent_execution'
908
- AND pu.used_at > NOW() - INTERVAL '30 days'
909
- GROUP BY pu.pattern_id
910
- ) sub
911
- WHERE p.pattern_id = sub.pattern_id
912
- `;
913
-
914
- try {
915
- const result = await executeQuery(query);
916
- return result.rowCount || 0;
917
- } catch (error) {
918
- console.error('[CorrelationAnalyzer] Agent correlation update failed:', error.message);
919
- return 0;
920
- }
921
- }
922
-
923
- /**
924
- * Get configuration (for debugging/admin)
925
- */
926
- getConfiguration() {
927
- return this.config;
928
- }
929
- }
930
-
931
- module.exports = { CorrelationAnalyzer, DEFAULT_CONFIG };