@equilateral_ai/mindmeld 3.5.3 → 4.0.1

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