@equilateral_ai/mindmeld 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. package/src/index.js +969 -0
@@ -0,0 +1,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
+ });