@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,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team Load-Bearing Detector
|
|
3
|
+
*
|
|
4
|
+
* Extends LoadBearingDetector to aggregate across multiple team members.
|
|
5
|
+
*
|
|
6
|
+
* Key insight: Load-bearing context detection is more accurate when analyzing
|
|
7
|
+
* patterns across the entire team, not just individual users.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* - User A: coordinate_system present = 90% success (10 handoffs)
|
|
11
|
+
* - User B: coordinate_system present = 92% success (15 handoffs)
|
|
12
|
+
* - Team aggregate: coordinate_system present = 91.2% success (25 handoffs)
|
|
13
|
+
* → Higher confidence in load-bearing classification
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const LoadBearingDetector = require('./LoadBearingDetector');
|
|
17
|
+
|
|
18
|
+
class TeamLoadBearingDetector {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
// Configuration
|
|
21
|
+
this.config = {
|
|
22
|
+
correlationThreshold: config.correlationThreshold || 0.7,
|
|
23
|
+
minObservations: config.minObservations || 10, // Higher threshold for team
|
|
24
|
+
minTeamMembers: config.minTeamMembers || 2, // Need 2+ users for team analysis
|
|
25
|
+
...config
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Per-user detectors
|
|
29
|
+
this.userDetectors = new Map(); // userId -> LoadBearingDetector
|
|
30
|
+
|
|
31
|
+
// Team-wide aggregation
|
|
32
|
+
this.teamLoadBearing = new Map(); // element key -> team statistics
|
|
33
|
+
|
|
34
|
+
// Team members
|
|
35
|
+
this.teamMembers = new Set();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get or create detector for user
|
|
40
|
+
*/
|
|
41
|
+
getDetectorForUser(userId) {
|
|
42
|
+
if (!this.userDetectors.has(userId)) {
|
|
43
|
+
this.userDetectors.set(userId, new LoadBearingDetector());
|
|
44
|
+
this.teamMembers.add(userId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.userDetectors.get(userId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Analyze handoff for specific user
|
|
52
|
+
*/
|
|
53
|
+
analyzeHandoff(userId, handoff, outcome) {
|
|
54
|
+
const detector = this.getDetectorForUser(userId);
|
|
55
|
+
|
|
56
|
+
// Analyze for individual user
|
|
57
|
+
const result = detector.analyzeHandoff(handoff, outcome);
|
|
58
|
+
|
|
59
|
+
// Update team-wide aggregation
|
|
60
|
+
this.aggregateTeamStatistics();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...result,
|
|
64
|
+
teamLoadBearing: this.getTeamLoadBearing()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Aggregate load-bearing statistics across all team members
|
|
70
|
+
*/
|
|
71
|
+
aggregateTeamStatistics() {
|
|
72
|
+
this.teamLoadBearing.clear();
|
|
73
|
+
|
|
74
|
+
// Collect all context elements from all users
|
|
75
|
+
for (const [userId, detector] of this.userDetectors.entries()) {
|
|
76
|
+
for (const [key, stats] of detector.contextElements.entries()) {
|
|
77
|
+
// Get or create team stats for this element
|
|
78
|
+
if (!this.teamLoadBearing.has(key)) {
|
|
79
|
+
this.teamLoadBearing.set(key, {
|
|
80
|
+
element: stats.element,
|
|
81
|
+
userStats: new Map(), // userId -> user's stats for this element
|
|
82
|
+
aggregated: {
|
|
83
|
+
observations: 0,
|
|
84
|
+
presentAndSuccess: 0,
|
|
85
|
+
presentAndFailure: 0,
|
|
86
|
+
totalPresent: 0,
|
|
87
|
+
userCount: 0
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const teamStats = this.teamLoadBearing.get(key);
|
|
93
|
+
|
|
94
|
+
// Store user's stats
|
|
95
|
+
teamStats.userStats.set(userId, stats);
|
|
96
|
+
|
|
97
|
+
// Aggregate across team
|
|
98
|
+
teamStats.aggregated.observations += stats.observations;
|
|
99
|
+
teamStats.aggregated.presentAndSuccess += stats.presentAndSuccess || 0;
|
|
100
|
+
teamStats.aggregated.presentAndFailure += stats.presentAndFailure || 0;
|
|
101
|
+
teamStats.aggregated.totalPresent += stats.totalPresent || 0;
|
|
102
|
+
teamStats.aggregated.userCount = teamStats.userStats.size;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get team-wide load-bearing elements
|
|
109
|
+
*/
|
|
110
|
+
getTeamLoadBearing() {
|
|
111
|
+
const loadBearing = [];
|
|
112
|
+
|
|
113
|
+
for (const [key, teamStats] of this.teamLoadBearing.entries()) {
|
|
114
|
+
const agg = teamStats.aggregated;
|
|
115
|
+
|
|
116
|
+
// Check minimum observations
|
|
117
|
+
if (agg.observations < this.config.minObservations) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check minimum team members
|
|
122
|
+
if (agg.userCount < this.config.minTeamMembers) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Calculate team correlation
|
|
127
|
+
const correlation = agg.totalPresent > 0
|
|
128
|
+
? agg.presentAndSuccess / agg.totalPresent
|
|
129
|
+
: 0;
|
|
130
|
+
|
|
131
|
+
if (correlation >= this.config.correlationThreshold) {
|
|
132
|
+
loadBearing.push({
|
|
133
|
+
element: teamStats.element,
|
|
134
|
+
correlation,
|
|
135
|
+
observations: agg.observations,
|
|
136
|
+
teamSize: agg.userCount,
|
|
137
|
+
confidence: this.calculateTeamConfidence(teamStats),
|
|
138
|
+
userBreakdown: this.getUserBreakdown(teamStats),
|
|
139
|
+
recommendation: 'LOAD_BEARING'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return loadBearing.sort((a, b) => b.correlation - a.correlation);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Calculate confidence in load-bearing classification based on team agreement
|
|
149
|
+
*/
|
|
150
|
+
calculateTeamConfidence(teamStats) {
|
|
151
|
+
const agg = teamStats.aggregated;
|
|
152
|
+
|
|
153
|
+
// Factor 1: Observation count (more = higher confidence)
|
|
154
|
+
const observationConfidence = Math.min(agg.observations / 50, 1.0);
|
|
155
|
+
|
|
156
|
+
// Factor 2: Team size (more users = higher confidence)
|
|
157
|
+
const teamSizeConfidence = Math.min(agg.userCount / 5, 1.0);
|
|
158
|
+
|
|
159
|
+
// Factor 3: User agreement (all users see similar correlation)
|
|
160
|
+
const agreementConfidence = this.calculateUserAgreement(teamStats);
|
|
161
|
+
|
|
162
|
+
// Factor 4: Correlation strength
|
|
163
|
+
const correlation = agg.totalPresent > 0
|
|
164
|
+
? agg.presentAndSuccess / agg.totalPresent
|
|
165
|
+
: 0;
|
|
166
|
+
|
|
167
|
+
// Weighted average
|
|
168
|
+
return (
|
|
169
|
+
observationConfidence * 0.2 +
|
|
170
|
+
teamSizeConfidence * 0.2 +
|
|
171
|
+
agreementConfidence * 0.3 +
|
|
172
|
+
correlation * 0.3
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Calculate agreement across team members
|
|
178
|
+
*
|
|
179
|
+
* High agreement = all users see similar correlation
|
|
180
|
+
* Low agreement = users see different results
|
|
181
|
+
*/
|
|
182
|
+
calculateUserAgreement(teamStats) {
|
|
183
|
+
const userCorrelations = [];
|
|
184
|
+
|
|
185
|
+
for (const [userId, stats] of teamStats.userStats.entries()) {
|
|
186
|
+
if (stats.totalPresent > 0) {
|
|
187
|
+
const correlation = stats.presentAndSuccess / stats.totalPresent;
|
|
188
|
+
userCorrelations.push(correlation);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (userCorrelations.length < 2) {
|
|
193
|
+
return 0.5; // Not enough data for agreement
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Calculate standard deviation
|
|
197
|
+
const mean = userCorrelations.reduce((a, b) => a + b, 0) / userCorrelations.length;
|
|
198
|
+
const variance = userCorrelations.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / userCorrelations.length;
|
|
199
|
+
const stdDev = Math.sqrt(variance);
|
|
200
|
+
|
|
201
|
+
// Low std dev = high agreement
|
|
202
|
+
// Agreement score: 1.0 - stdDev (capped at 0)
|
|
203
|
+
return Math.max(0, 1.0 - stdDev);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get breakdown by user
|
|
208
|
+
*/
|
|
209
|
+
getUserBreakdown(teamStats) {
|
|
210
|
+
const breakdown = [];
|
|
211
|
+
|
|
212
|
+
for (const [userId, stats] of teamStats.userStats.entries()) {
|
|
213
|
+
const correlation = stats.totalPresent > 0
|
|
214
|
+
? stats.presentAndSuccess / stats.totalPresent
|
|
215
|
+
: 0;
|
|
216
|
+
|
|
217
|
+
breakdown.push({
|
|
218
|
+
userId,
|
|
219
|
+
observations: stats.observations,
|
|
220
|
+
correlation,
|
|
221
|
+
presentAndSuccess: stats.presentAndSuccess,
|
|
222
|
+
presentAndFailure: stats.presentAndFailure
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return breakdown.sort((a, b) => b.correlation - a.correlation);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Apply team recommendations to decision frame
|
|
231
|
+
*/
|
|
232
|
+
applyTeamRecommendations(decisionFrame, userId) {
|
|
233
|
+
const loadBearing = this.getTeamLoadBearing();
|
|
234
|
+
const recommendations = this.generateTeamRecommendations(loadBearing);
|
|
235
|
+
|
|
236
|
+
// Use individual user's detector to apply
|
|
237
|
+
const detector = this.getDetectorForUser(userId);
|
|
238
|
+
return detector.applyRecommendations(decisionFrame, recommendations);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate recommendations with team context
|
|
243
|
+
*/
|
|
244
|
+
generateTeamRecommendations(loadBearing) {
|
|
245
|
+
const recommendations = [];
|
|
246
|
+
|
|
247
|
+
for (const lb of loadBearing) {
|
|
248
|
+
if (lb.element.type === 'constraint') {
|
|
249
|
+
recommendations.push({
|
|
250
|
+
type: 'mark_constraint_load_bearing',
|
|
251
|
+
constraint: lb.element.value,
|
|
252
|
+
reason: `Team-wide: ${(lb.correlation * 100).toFixed(1)}% success rate across ${lb.teamSize} users (${lb.observations} observations)`,
|
|
253
|
+
confidence: lb.confidence,
|
|
254
|
+
teamSize: lb.teamSize
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (lb.element.type === 'context_key') {
|
|
259
|
+
recommendations.push({
|
|
260
|
+
type: 'always_include_context',
|
|
261
|
+
key: lb.element.key,
|
|
262
|
+
reason: `Team-wide: ${(lb.correlation * 100).toFixed(1)}% success rate across ${lb.teamSize} users (${lb.observations} observations)`,
|
|
263
|
+
confidence: lb.confidence,
|
|
264
|
+
teamSize: lb.teamSize
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (lb.element.type === 'missing') {
|
|
269
|
+
recommendations.push({
|
|
270
|
+
type: 'prevent_missing_context',
|
|
271
|
+
key: lb.element.key,
|
|
272
|
+
reason: `Team-wide: ${(lb.correlation * 100).toFixed(1)}% failure rate when missing across ${lb.teamSize} users (${lb.observations} observations)`,
|
|
273
|
+
confidence: lb.confidence,
|
|
274
|
+
teamSize: lb.teamSize
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return recommendations;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get patterns shared across team
|
|
284
|
+
*/
|
|
285
|
+
getSharedPatterns() {
|
|
286
|
+
const shared = [];
|
|
287
|
+
|
|
288
|
+
for (const [key, teamStats] of this.teamLoadBearing.entries()) {
|
|
289
|
+
if (teamStats.aggregated.userCount >= 2) {
|
|
290
|
+
const agg = teamStats.aggregated;
|
|
291
|
+
const correlation = agg.totalPresent > 0
|
|
292
|
+
? agg.presentAndSuccess / agg.totalPresent
|
|
293
|
+
: 0;
|
|
294
|
+
|
|
295
|
+
if (correlation >= this.config.correlationThreshold) {
|
|
296
|
+
shared.push({
|
|
297
|
+
element: teamStats.element,
|
|
298
|
+
sharedBy: Array.from(teamStats.userStats.keys()),
|
|
299
|
+
correlation,
|
|
300
|
+
observations: agg.observations
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return shared.sort((a, b) => b.sharedBy.length - a.sharedBy.length);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get load-bearing elements discovered by one user but applicable to all
|
|
311
|
+
*/
|
|
312
|
+
getCrossPollination() {
|
|
313
|
+
const crossPollinated = [];
|
|
314
|
+
|
|
315
|
+
for (const [key, teamStats] of this.teamLoadBearing.entries()) {
|
|
316
|
+
// Found by one user, but high correlation
|
|
317
|
+
if (teamStats.aggregated.userCount === 1) {
|
|
318
|
+
const userId = Array.from(teamStats.userStats.keys())[0];
|
|
319
|
+
const stats = teamStats.userStats.get(userId);
|
|
320
|
+
|
|
321
|
+
const correlation = stats.totalPresent > 0
|
|
322
|
+
? stats.presentAndSuccess / stats.totalPresent
|
|
323
|
+
: 0;
|
|
324
|
+
|
|
325
|
+
if (correlation >= this.config.correlationThreshold && stats.observations >= 5) {
|
|
326
|
+
crossPollinated.push({
|
|
327
|
+
element: teamStats.element,
|
|
328
|
+
discoveredBy: userId,
|
|
329
|
+
correlation,
|
|
330
|
+
observations: stats.observations,
|
|
331
|
+
potentialForTeam: true,
|
|
332
|
+
recommendation: 'Share with team - high correlation in solo use'
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return crossPollinated.sort((a, b) => b.correlation - a.correlation);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get team summary
|
|
343
|
+
*/
|
|
344
|
+
getTeamSummary() {
|
|
345
|
+
const summary = {
|
|
346
|
+
teamSize: this.teamMembers.size,
|
|
347
|
+
totalHandoffs: 0,
|
|
348
|
+
successfulHandoffs: 0,
|
|
349
|
+
teamLoadBearingElements: this.getTeamLoadBearing().length,
|
|
350
|
+
sharedPatterns: this.getSharedPatterns().length,
|
|
351
|
+
crossPollination: this.getCrossPollination().length,
|
|
352
|
+
byUser: {}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Aggregate by user
|
|
356
|
+
for (const [userId, detector] of this.userDetectors.entries()) {
|
|
357
|
+
const userSummary = detector.getSummary();
|
|
358
|
+
summary.totalHandoffs += userSummary.totalHandoffs;
|
|
359
|
+
summary.successfulHandoffs += userSummary.successfulHandoffs;
|
|
360
|
+
|
|
361
|
+
summary.byUser[userId] = {
|
|
362
|
+
handoffs: userSummary.totalHandoffs,
|
|
363
|
+
successRate: userSummary.totalHandoffs > 0
|
|
364
|
+
? userSummary.successfulHandoffs / userSummary.totalHandoffs
|
|
365
|
+
: 0,
|
|
366
|
+
loadBearingDetected: userSummary.loadBearingDetected
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return summary;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Export team data
|
|
375
|
+
*/
|
|
376
|
+
exportTeamData() {
|
|
377
|
+
return {
|
|
378
|
+
teamMembers: Array.from(this.teamMembers),
|
|
379
|
+
userDetectors: Object.fromEntries(
|
|
380
|
+
Array.from(this.userDetectors.entries()).map(([userId, detector]) => [
|
|
381
|
+
userId,
|
|
382
|
+
detector.exportData()
|
|
383
|
+
])
|
|
384
|
+
),
|
|
385
|
+
teamLoadBearing: Array.from(this.teamLoadBearing.entries()).map(([key, stats]) => ({
|
|
386
|
+
key,
|
|
387
|
+
element: stats.element,
|
|
388
|
+
aggregated: stats.aggregated,
|
|
389
|
+
userBreakdown: this.getUserBreakdown(stats)
|
|
390
|
+
}))
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Import team data
|
|
396
|
+
*/
|
|
397
|
+
importTeamData(exported) {
|
|
398
|
+
// Import team members
|
|
399
|
+
if (exported.teamMembers) {
|
|
400
|
+
for (const userId of exported.teamMembers) {
|
|
401
|
+
this.teamMembers.add(userId);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Import user detectors
|
|
406
|
+
if (exported.userDetectors) {
|
|
407
|
+
for (const [userId, detectorData] of Object.entries(exported.userDetectors)) {
|
|
408
|
+
const detector = new LoadBearingDetector();
|
|
409
|
+
|
|
410
|
+
// Import handoff analysis
|
|
411
|
+
if (detectorData.handoffAnalysis) {
|
|
412
|
+
detector.handoffAnalysis = detectorData.handoffAnalysis;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Import context elements
|
|
416
|
+
if (detectorData.contextElements) {
|
|
417
|
+
for (const elem of detectorData.contextElements) {
|
|
418
|
+
detector.contextElements.set(elem.key, elem);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.userDetectors.set(userId, detector);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Re-aggregate
|
|
427
|
+
this.aggregateTeamStatistics();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = TeamLoadBearingDetector;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rapport v3 - Database Operations
|
|
3
|
+
*
|
|
4
|
+
* Provides database query execution following Tim-Combo backend_handler_standards.md
|
|
5
|
+
* Uses cached single client (NOT connection pools) per Lambda database standards
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Client } = require('pg');
|
|
9
|
+
|
|
10
|
+
// Cached client for connection reuse across warm Lambda invocations
|
|
11
|
+
let cachedClient = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get or create database client
|
|
15
|
+
*
|
|
16
|
+
* Follows Lambda database standards:
|
|
17
|
+
* - NEVER use connection pools (Lambda handles one request at a time)
|
|
18
|
+
* - Cache single client for reuse across warm invocations
|
|
19
|
+
* - Use environment variables (NOT runtime SSM fetches)
|
|
20
|
+
*
|
|
21
|
+
* @returns {Promise<Client>} PostgreSQL client
|
|
22
|
+
*/
|
|
23
|
+
async function getClient() {
|
|
24
|
+
if (cachedClient && cachedClient._connected) {
|
|
25
|
+
return cachedClient;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const client = new Client({
|
|
29
|
+
host: process.env.DB_HOST,
|
|
30
|
+
port: process.env.DB_PORT || 5432,
|
|
31
|
+
database: process.env.DB_NAME,
|
|
32
|
+
user: process.env.DB_USER,
|
|
33
|
+
password: process.env.DB_PASSWORD,
|
|
34
|
+
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await client.connect();
|
|
38
|
+
client._connected = true;
|
|
39
|
+
|
|
40
|
+
cachedClient = client;
|
|
41
|
+
return client;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute a database query
|
|
46
|
+
*
|
|
47
|
+
* Standard pattern from backend_handler_standards.md
|
|
48
|
+
*
|
|
49
|
+
* @param {string} query - SQL query
|
|
50
|
+
* @param {Array} params - Query parameters
|
|
51
|
+
* @returns {Promise<Object>} Query result
|
|
52
|
+
*/
|
|
53
|
+
async function executeQuery(query, params = []) {
|
|
54
|
+
const client = await getClient();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await client.query(query, params);
|
|
58
|
+
return result;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('[dbOperations] Query error:', {
|
|
61
|
+
error: error.message,
|
|
62
|
+
query: query.substring(0, 200),
|
|
63
|
+
params: params
|
|
64
|
+
});
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute multiple queries in a transaction
|
|
71
|
+
*
|
|
72
|
+
* @param {Function} callback - Async function that receives client
|
|
73
|
+
* @returns {Promise<any>} Callback result
|
|
74
|
+
*/
|
|
75
|
+
async function executeTransaction(callback) {
|
|
76
|
+
const client = await getClient();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await client.query('BEGIN');
|
|
80
|
+
const result = await callback(client);
|
|
81
|
+
await client.query('COMMIT');
|
|
82
|
+
return result;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
await client.query('ROLLBACK');
|
|
85
|
+
console.error('[dbOperations] Transaction error:', error);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Close database connection (for graceful shutdown)
|
|
92
|
+
*/
|
|
93
|
+
async function closeConnection() {
|
|
94
|
+
if (cachedClient) {
|
|
95
|
+
await cachedClient.end();
|
|
96
|
+
cachedClient = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
executeQuery,
|
|
102
|
+
executeTransaction,
|
|
103
|
+
closeConnection,
|
|
104
|
+
getClient
|
|
105
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Get Me Handler
|
|
3
|
+
* Retrieves current user's activity summary
|
|
4
|
+
*
|
|
5
|
+
* GET /api/activity/me
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
exports.handler = wrapHandler(async ({ requestContext }) => {
|
|
12
|
+
const Request_ID = requestContext.requestId;
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Authentication required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Get activity from materialized view
|
|
20
|
+
const result = await executeQuery(`
|
|
21
|
+
SELECT
|
|
22
|
+
email_address,
|
|
23
|
+
display_name,
|
|
24
|
+
sessions_30d,
|
|
25
|
+
sessions_7d,
|
|
26
|
+
last_session,
|
|
27
|
+
avg_session_duration_seconds,
|
|
28
|
+
commits_30d,
|
|
29
|
+
commits_7d,
|
|
30
|
+
last_commit,
|
|
31
|
+
days_since_commit,
|
|
32
|
+
prs_opened_30d,
|
|
33
|
+
prs_merged_30d,
|
|
34
|
+
avg_pr_review_hours,
|
|
35
|
+
commits_within_24h_of_session,
|
|
36
|
+
session_to_commit_conversion_pct
|
|
37
|
+
FROM rapport.mv_developer_activity
|
|
38
|
+
WHERE email_address = $1
|
|
39
|
+
`, [email]);
|
|
40
|
+
|
|
41
|
+
if (result.rowCount === 0) {
|
|
42
|
+
// Return empty activity for new users
|
|
43
|
+
return createSuccessResponse(
|
|
44
|
+
{
|
|
45
|
+
Records: [{
|
|
46
|
+
email_address: email,
|
|
47
|
+
sessions_30d: 0,
|
|
48
|
+
sessions_7d: 0,
|
|
49
|
+
commits_30d: 0,
|
|
50
|
+
commits_7d: 0,
|
|
51
|
+
prs_opened_30d: 0,
|
|
52
|
+
prs_merged_30d: 0,
|
|
53
|
+
session_to_commit_conversion_pct: 0
|
|
54
|
+
}]
|
|
55
|
+
},
|
|
56
|
+
'Activity retrieved (new user)',
|
|
57
|
+
{ Total_Records: 1, Request_ID, Timestamp: new Date().toISOString() }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const activity = result.rows[0];
|
|
62
|
+
|
|
63
|
+
return createSuccessResponse(
|
|
64
|
+
{
|
|
65
|
+
Records: [{
|
|
66
|
+
email_address: activity.email_address,
|
|
67
|
+
display_name: activity.display_name,
|
|
68
|
+
sessions: {
|
|
69
|
+
last_30_days: parseInt(activity.sessions_30d) || 0,
|
|
70
|
+
last_7_days: parseInt(activity.sessions_7d) || 0,
|
|
71
|
+
last_session: activity.last_session,
|
|
72
|
+
avg_duration_minutes: activity.avg_session_duration_seconds
|
|
73
|
+
? Math.round(activity.avg_session_duration_seconds / 60)
|
|
74
|
+
: null
|
|
75
|
+
},
|
|
76
|
+
commits: {
|
|
77
|
+
last_30_days: parseInt(activity.commits_30d) || 0,
|
|
78
|
+
last_7_days: parseInt(activity.commits_7d) || 0,
|
|
79
|
+
last_commit: activity.last_commit,
|
|
80
|
+
days_since_commit: parseInt(activity.days_since_commit) || null
|
|
81
|
+
},
|
|
82
|
+
pull_requests: {
|
|
83
|
+
opened_30d: parseInt(activity.prs_opened_30d) || 0,
|
|
84
|
+
merged_30d: parseInt(activity.prs_merged_30d) || 0,
|
|
85
|
+
avg_review_hours: activity.avg_pr_review_hours
|
|
86
|
+
? parseFloat(activity.avg_pr_review_hours).toFixed(1)
|
|
87
|
+
: null
|
|
88
|
+
},
|
|
89
|
+
value_correlation: {
|
|
90
|
+
commits_within_24h_of_session: parseInt(activity.commits_within_24h_of_session) || 0,
|
|
91
|
+
session_to_commit_conversion_pct: parseFloat(activity.session_to_commit_conversion_pct) || 0
|
|
92
|
+
}
|
|
93
|
+
}]
|
|
94
|
+
},
|
|
95
|
+
'Activity retrieved',
|
|
96
|
+
{ Total_Records: 1, Request_ID, Timestamp: new Date().toISOString() }
|
|
97
|
+
);
|
|
98
|
+
});
|