@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.
- package/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- 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 };
|