@equilateral_ai/mindmeld 3.5.2 → 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 (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,361 +0,0 @@
1
- /**
2
- * Developer Coaching Recommendations Handler
3
- * LLM-powered coaching insights per developer
4
- *
5
- * GET /api/analytics/coaching?user_id=xxx&project_id=xxx
6
- * Query params:
7
- * - user_id (required) - Developer email to generate coaching for
8
- * - project_id (required) - Project to scope coaching data
9
- *
10
- * Returns:
11
- * - user_email: developer email
12
- * - recommendations: LLM-generated coaching items with categories and evidence
13
- * - strengths: identified developer strengths
14
- * - generated_at: timestamp
15
- *
16
- * Auth: Cognito JWT required, Manager or Admin role (or self)
17
- */
18
-
19
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
20
- const { LLMPatternDetector } = require('./core/LLMPatternDetector');
21
-
22
- async function getCoachingRecommendations({ requestContext, queryStringParameters }) {
23
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
24
-
25
- if (!email) {
26
- return createErrorResponse(401, 'Unauthorized');
27
- }
28
-
29
- const params = queryStringParameters || {};
30
- const projectId = params.project_id;
31
- const targetUserId = params.user_id;
32
-
33
- if (!projectId) {
34
- return createErrorResponse(400, 'project_id is required');
35
- }
36
-
37
- if (!targetUserId) {
38
- return createErrorResponse(400, 'user_id is required');
39
- }
40
-
41
- // Check access: self-access or manager/admin
42
- const isSelf = email.toLowerCase() === targetUserId.toLowerCase();
43
-
44
- if (!isSelf) {
45
- const accessCheck = await executeQuery(`
46
- SELECT ue.company_id
47
- FROM rapport.user_entitlements ue
48
- JOIN rapport.projects p ON p.company_id = ue.company_id
49
- WHERE ue.email_address = $1
50
- AND p.project_id = $2
51
- AND (ue.admin = true OR ue.manager = true)
52
- LIMIT 1
53
- `, [email, projectId]);
54
-
55
- if (accessCheck.rows.length === 0) {
56
- return createErrorResponse(403, 'Manager or Admin access required to view other developers');
57
- }
58
- }
59
-
60
- // Verify target user exists and belongs to the project's company
61
- const userResult = await executeQuery(`
62
- SELECT
63
- u.email_address as user_email,
64
- CONCAT(u.first_name, ' ', u.last_name) as display_name
65
- FROM rapport.users u
66
- JOIN rapport.user_entitlements ue ON ue.email_address = u.email_address
67
- JOIN rapport.projects p ON p.company_id = ue.company_id
68
- WHERE u.email_address = $1
69
- AND p.project_id = $2
70
- LIMIT 1
71
- `, [targetUserId, projectId]);
72
-
73
- if (userResult.rows.length === 0) {
74
- return createErrorResponse(404, 'Developer not found in this project');
75
- }
76
-
77
- const targetUser = userResult.rows[0];
78
-
79
- // Fetch violation data for the developer (last 90 days for comprehensive coaching)
80
- const lookbackDate = new Date();
81
- lookbackDate.setDate(lookbackDate.getDate() - 90);
82
-
83
- const violationResult = await executeQuery(`
84
- SELECT
85
- pv.category,
86
- pv.severity,
87
- pv.description,
88
- pv.standard_name,
89
- pv.detected_at,
90
- COUNT(*) OVER (PARTITION BY pv.category) as category_count
91
- FROM rapport.pattern_violations pv
92
- WHERE pv.developer_email = $1
93
- AND pv.project_id = $2
94
- AND pv.detected_at >= $3
95
- ORDER BY pv.detected_at DESC
96
- `, [targetUserId, projectId, lookbackDate]);
97
-
98
- // Fetch pattern usage data
99
- const patternResult = await executeQuery(`
100
- SELECT
101
- p.intent,
102
- p.maturity,
103
- COUNT(DISTINCT pu.session_id) as sessions_used,
104
- MAX(pu.used_at) as last_used
105
- FROM rapport.pattern_usage pu
106
- JOIN rapport.patterns p ON pu.pattern_id = p.pattern_id
107
- WHERE pu.email_address = $1
108
- AND pu.project_id = $2
109
- AND pu.used_at >= $3
110
- GROUP BY p.intent, p.maturity
111
- ORDER BY sessions_used DESC
112
- `, [targetUserId, projectId, lookbackDate]);
113
-
114
- // Fetch compliance metrics
115
- const metricsResult = await executeQuery(`
116
- SELECT
117
- AVG(dm.compliance_score) as avg_compliance,
118
- SUM(dm.commit_count) as total_commits,
119
- SUM(dm.standards_compliant_commits) as compliant_commits
120
- FROM rapport.developer_metrics dm
121
- WHERE dm.developer_email = $1
122
- AND dm.project_id = $2
123
- AND dm.period_start >= $3
124
- `, [targetUserId, projectId, lookbackDate]);
125
-
126
- const metrics = metricsResult.rows[0] || {};
127
- const violations = violationResult.rows;
128
- const patterns = patternResult.rows;
129
-
130
- // Attempt LLM-based coaching recommendations
131
- let recommendations = [];
132
- let strengths = [];
133
-
134
- const detector = new LLMPatternDetector();
135
-
136
- if (detector.isAvailable()) {
137
- try {
138
- const coachingResult = await generateLLMCoaching(detector, {
139
- userEmail: targetUserId,
140
- displayName: targetUser.display_name,
141
- violations,
142
- patterns,
143
- metrics,
144
- projectId
145
- });
146
-
147
- recommendations = coachingResult.recommendations || [];
148
- strengths = coachingResult.strengths || [];
149
- } catch (error) {
150
- console.error('[CoachingGet] LLM coaching generation failed, falling back to rule-based:', error.message);
151
- const fallback = generateRuleBasedCoaching(violations, patterns, metrics);
152
- recommendations = fallback.recommendations;
153
- strengths = fallback.strengths;
154
- }
155
- } else {
156
- // Fallback: rule-based recommendations from violation patterns
157
- const fallback = generateRuleBasedCoaching(violations, patterns, metrics);
158
- recommendations = fallback.recommendations;
159
- strengths = fallback.strengths;
160
- }
161
-
162
- return createSuccessResponse({
163
- user_email: targetUser.user_email,
164
- recommendations,
165
- strengths,
166
- generated_at: new Date().toISOString()
167
- }, 'Coaching recommendations generated');
168
- }
169
-
170
- exports.handler = wrapHandler(getCoachingRecommendations);
171
-
172
- /**
173
- * Generate coaching recommendations using LLM
174
- */
175
- async function generateLLMCoaching(detector, context) {
176
- const violationSummary = summarizeViolations(context.violations);
177
- const patternSummary = summarizePatterns(context.patterns);
178
- const complianceAvg = parseFloat(context.metrics.avg_compliance || 0).toFixed(1);
179
-
180
- const transcript = `
181
- Developer Coaching Analysis for ${context.displayName || context.userEmail}:
182
-
183
- COMPLIANCE METRICS:
184
- - Average compliance score: ${complianceAvg}%
185
- - Total commits: ${context.metrics.total_commits || 0}
186
- - Standards-compliant commits: ${context.metrics.compliant_commits || 0}
187
-
188
- VIOLATIONS (last 90 days):
189
- ${violationSummary}
190
-
191
- PATTERN USAGE (last 90 days):
192
- ${patternSummary}
193
-
194
- Generate coaching recommendations with:
195
- 1. Specific, actionable items categorized by area (security, performance, architecture, etc.)
196
- 2. Priority level (high, medium, low) based on violation frequency and severity
197
- 3. Suggested standards to review
198
- 4. Evidence from their actual violation and pattern data
199
- 5. Identified strengths based on pattern adoption and compliance
200
-
201
- Respond with JSON:
202
- {
203
- "recommendations": [
204
- {
205
- "category": "category_name",
206
- "priority": "high|medium|low",
207
- "title": "Short coaching title",
208
- "description": "Detailed coaching recommendation",
209
- "suggested_standards": ["standard1", "standard2"],
210
- "evidence": { "violations": 0, "most_recent": "date" }
211
- }
212
- ],
213
- "strengths": ["Strength 1", "Strength 2"]
214
- }`;
215
-
216
- const result = await detector.analyzeSessionTranscript(transcript, {
217
- projectName: `Project ${context.projectId}`,
218
- techStack: 'Node.js, AWS Lambda, PostgreSQL, React'
219
- });
220
-
221
- // Parse LLM response for coaching-specific format
222
- if (result.success && result.recommendations) {
223
- return {
224
- recommendations: Array.isArray(result.recommendations)
225
- ? result.recommendations.map(rec => ({
226
- category: rec.category || 'general',
227
- priority: rec.priority || 'medium',
228
- title: rec.title || rec.action || 'Recommendation',
229
- description: rec.description || rec.details || '',
230
- suggested_standards: rec.suggested_standards || [],
231
- evidence: rec.evidence || {}
232
- }))
233
- : [],
234
- strengths: result.strengths || []
235
- };
236
- }
237
-
238
- // If LLM returned patterns instead of coaching format, adapt
239
- return generateRuleBasedCoaching(context.violations, context.patterns, context.metrics);
240
- }
241
-
242
- /**
243
- * Generate rule-based coaching recommendations from violation patterns
244
- */
245
- function generateRuleBasedCoaching(violations, patterns, metrics) {
246
- const recommendations = [];
247
- const strengths = [];
248
-
249
- // Group violations by category
250
- const violationsByCategory = {};
251
- for (const v of violations) {
252
- if (!violationsByCategory[v.category]) {
253
- violationsByCategory[v.category] = [];
254
- }
255
- violationsByCategory[v.category].push(v);
256
- }
257
-
258
- // Generate recommendations from violations
259
- for (const [category, categoryViolations] of Object.entries(violationsByCategory)) {
260
- const count = categoryViolations.length;
261
- const mostRecent = categoryViolations[0]?.detected_at;
262
- const uniqueStandards = [...new Set(categoryViolations.map(v => v.standard_name).filter(Boolean))];
263
-
264
- let priority = 'low';
265
- if (count >= 5 || categoryViolations.some(v => v.severity === 'critical')) {
266
- priority = 'high';
267
- } else if (count >= 2 || categoryViolations.some(v => v.severity === 'warning')) {
268
- priority = 'medium';
269
- }
270
-
271
- const titles = {
272
- security: 'Improve security practices',
273
- performance: 'Address performance anti-patterns',
274
- database: 'Strengthen database access patterns',
275
- api: 'Align with API design standards',
276
- architecture: 'Follow architectural guidelines',
277
- 'error-handling': 'Enhance error handling consistency'
278
- };
279
-
280
- const descriptions = {
281
- security: `This developer has ${count} security-related violation(s) in the last 90 days. Review input validation, secrets management, and authorization patterns.`,
282
- performance: `${count} performance issue(s) detected. Focus on connection management, caching strategies, and avoiding runtime SSM lookups.`,
283
- database: `${count} database-related violation(s) found. Ensure use of cached single client pattern and parameterized queries.`,
284
- api: `${count} API design violation(s). Review response formatting, CORS configuration, and handler patterns.`,
285
- architecture: `${count} architectural violation(s). Check adherence to business-scoped IDs, module boundaries, and code organization.`,
286
- 'error-handling': `${count} error handling issue(s). Ensure consistent use of wrapHandler and proper error response patterns.`
287
- };
288
-
289
- recommendations.push({
290
- category,
291
- priority,
292
- title: titles[category] || `Improve ${category} practices`,
293
- description: descriptions[category] || `This developer has ${count} violation(s) related to ${category} in the last 90 days.`,
294
- suggested_standards: uniqueStandards.length > 0 ? uniqueStandards : [category],
295
- evidence: {
296
- violations: count,
297
- most_recent: mostRecent ? new Date(mostRecent).toISOString().split('T')[0] : null
298
- }
299
- });
300
- }
301
-
302
- // Sort recommendations by priority
303
- const priorityOrder = { high: 0, medium: 1, low: 2 };
304
- recommendations.sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2));
305
-
306
- // Identify strengths from pattern usage
307
- const highAdoptionPatterns = patterns.filter(p => parseInt(p.sessions_used) >= 5);
308
- if (highAdoptionPatterns.length > 0) {
309
- strengths.push(`Consistent use of ${highAdoptionPatterns.length} team pattern(s)`);
310
- }
311
-
312
- const validatedPatterns = patterns.filter(p => p.maturity === 'validated' || p.maturity === 'reinforced');
313
- if (validatedPatterns.length > 0) {
314
- strengths.push(`Active contributor to ${validatedPatterns.length} validated pattern(s)`);
315
- }
316
-
317
- const complianceScore = parseFloat(metrics.avg_compliance || 0);
318
- if (complianceScore >= 80) {
319
- strengths.push('Strong standards adherence above 80%');
320
- }
321
-
322
- const totalCommits = parseInt(metrics.total_commits || 0);
323
- const compliantCommits = parseInt(metrics.compliant_commits || 0);
324
- if (totalCommits > 0 && (compliantCommits / totalCommits) >= 0.9) {
325
- strengths.push('Over 90% of commits meet standards compliance');
326
- }
327
-
328
- if (violations.length === 0 && totalCommits > 0) {
329
- strengths.push('Zero violations in the last 90 days');
330
- }
331
-
332
- return { recommendations, strengths };
333
- }
334
-
335
- /**
336
- * Summarize violations for LLM context
337
- */
338
- function summarizeViolations(violations) {
339
- if (violations.length === 0) return 'No violations detected in the last 90 days.';
340
-
341
- const byCategory = {};
342
- for (const v of violations) {
343
- if (!byCategory[v.category]) byCategory[v.category] = 0;
344
- byCategory[v.category]++;
345
- }
346
-
347
- return Object.entries(byCategory)
348
- .map(([cat, count]) => `- ${cat}: ${count} violation(s)`)
349
- .join('\n');
350
- }
351
-
352
- /**
353
- * Summarize patterns for LLM context
354
- */
355
- function summarizePatterns(patterns) {
356
- if (patterns.length === 0) return 'No pattern usage recorded.';
357
-
358
- return patterns.slice(0, 10)
359
- .map(p => `- ${p.intent} (${p.maturity}): used in ${p.sessions_used} session(s)`)
360
- .join('\n');
361
- }
@@ -1,236 +0,0 @@
1
- /**
2
- * Team Convergence Analytics Handler
3
- * Measures how consistently team members adopt the same standards
4
- *
5
- * GET /api/analytics/convergence
6
- * Query: ?period=30d&Company_ID=xxx
7
- * Auth: Cognito JWT required, Manager or Admin role
8
- *
9
- * Returns convergence score, per-standard adoption rates,
10
- * per-developer alignment scores, and weekly trend.
11
- */
12
-
13
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
14
-
15
- async function getConvergence({ requestContext, queryStringParameters }) {
16
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
17
-
18
- if (!email) {
19
- return createErrorResponse(401, 'Authentication required');
20
- }
21
-
22
- const params = queryStringParameters || {};
23
- const period = params.period || '30d';
24
- const companyId = params.Company_ID;
25
-
26
- // Validate manager/admin access
27
- const accessCheck = await executeQuery(`
28
- SELECT ue.company_id
29
- FROM rapport.user_entitlements ue
30
- WHERE ue.email_address = $1
31
- AND (ue.admin = true OR ue.manager = true)
32
- ${companyId ? 'AND ue.company_id = $2' : ''}
33
- LIMIT 1
34
- `, companyId ? [email, companyId] : [email]);
35
-
36
- if (accessCheck.rows.length === 0) {
37
- return createErrorResponse(403, 'Manager or Admin access required for convergence analytics');
38
- }
39
-
40
- const userCompanyId = companyId || accessCheck.rows[0].company_id;
41
- const periodDays = parsePeriod(period);
42
- const periodStart = new Date();
43
- periodStart.setDate(periodStart.getDate() - periodDays);
44
-
45
- // Get all active developers (those with sessions in period)
46
- let activeDevelopers = [];
47
- try {
48
- const devResult = await executeQuery(`
49
- SELECT DISTINCT s.email_address
50
- FROM rapport.sessions s
51
- JOIN rapport.projects p ON s.project_id = p.project_id
52
- WHERE p.company_id = $1
53
- AND s.started_at >= $2
54
- AND s.email_address IS NOT NULL
55
- `, [userCompanyId, periodStart]);
56
- activeDevelopers = devResult.rows.map(r => r.email_address);
57
- } catch (e) {
58
- console.log('[Convergence] Active developers query failed:', e.message);
59
- }
60
-
61
- if (activeDevelopers.length === 0) {
62
- return createSuccessResponse({
63
- convergence_score: 0,
64
- trend: 'stable',
65
- developer_count: 0,
66
- standards_in_use: 0,
67
- by_standard: [],
68
- by_developer: [],
69
- convergence_trend: []
70
- }, 'No active developers in period');
71
- }
72
-
73
- // Per-standard adoption: how many active devs received each standard
74
- let byStandard = [];
75
- try {
76
- const standardResult = await executeQuery(`
77
- SELECT
78
- ss.standard_id,
79
- ss.standard_name,
80
- COUNT(DISTINCT s.email_address) as developers_using,
81
- COUNT(*) as total_injections,
82
- ROUND(AVG(ss.relevance_score), 2) as avg_relevance
83
- FROM rapport.session_standards ss
84
- JOIN rapport.sessions s ON ss.session_id = s.session_id
85
- JOIN rapport.projects p ON s.project_id = p.project_id
86
- WHERE p.company_id = $1
87
- AND ss.created_at >= $2
88
- AND s.email_address IS NOT NULL
89
- GROUP BY ss.standard_id, ss.standard_name
90
- HAVING COUNT(DISTINCT s.email_address) >= 1
91
- ORDER BY developers_using DESC, total_injections DESC
92
- `, [userCompanyId, periodStart]);
93
-
94
- byStandard = standardResult.rows.map(row => ({
95
- standard_id: row.standard_id,
96
- standard_name: row.standard_name,
97
- adoption_rate: Math.round((parseInt(row.developers_using) / activeDevelopers.length) * 100),
98
- developers_using: parseInt(row.developers_using),
99
- total_injections: parseInt(row.total_injections),
100
- avg_relevance: parseFloat(row.avg_relevance) || 0
101
- }));
102
- } catch (e) {
103
- console.log('[Convergence] Per-standard query failed:', e.message);
104
- }
105
-
106
- // Per-developer: how many unique standards each dev uses
107
- let byDeveloper = [];
108
- try {
109
- const devStandardsResult = await executeQuery(`
110
- SELECT
111
- s.email_address,
112
- COALESCE(MAX(ue.display_name), s.email_address) as display_name,
113
- COUNT(DISTINCT ss.standard_id) as unique_standards,
114
- COUNT(*) as total_injections
115
- FROM rapport.session_standards ss
116
- JOIN rapport.sessions s ON ss.session_id = s.session_id
117
- JOIN rapport.projects p ON s.project_id = p.project_id
118
- LEFT JOIN rapport.user_entitlements ue ON s.email_address = ue.email_address AND ue.company_id = $1
119
- WHERE p.company_id = $1
120
- AND ss.created_at >= $2
121
- AND s.email_address IS NOT NULL
122
- GROUP BY s.email_address
123
- ORDER BY unique_standards DESC
124
- `, [userCompanyId, periodStart]);
125
-
126
- const totalUniqueStandards = byStandard.length;
127
-
128
- byDeveloper = devStandardsResult.rows.map(row => ({
129
- email: row.email_address,
130
- display_name: row.display_name,
131
- unique_standards: parseInt(row.unique_standards),
132
- total_injections: parseInt(row.total_injections),
133
- alignment_score: totalUniqueStandards > 0
134
- ? Math.round((parseInt(row.unique_standards) / totalUniqueStandards) * 100)
135
- : 0
136
- }));
137
- } catch (e) {
138
- console.log('[Convergence] Per-developer query failed:', e.message);
139
- }
140
-
141
- // Convergence score: average adoption rate across standards
142
- // High score = most standards are used by most developers
143
- const convergenceScore = byStandard.length > 0
144
- ? Math.round(byStandard.reduce((sum, s) => sum + s.adoption_rate, 0) / byStandard.length)
145
- : 0;
146
-
147
- // Weekly convergence trend (last 8 weeks or period, whichever is shorter)
148
- let convergenceTrend = [];
149
- try {
150
- const trendResult = await executeQuery(`
151
- WITH weekly_adoption AS (
152
- SELECT
153
- DATE_TRUNC('week', ss.created_at) as week,
154
- ss.standard_id,
155
- COUNT(DISTINCT s.email_address) as devs_using
156
- FROM rapport.session_standards ss
157
- JOIN rapport.sessions s ON ss.session_id = s.session_id
158
- JOIN rapport.projects p ON s.project_id = p.project_id
159
- WHERE p.company_id = $1
160
- AND ss.created_at >= $2
161
- AND s.email_address IS NOT NULL
162
- GROUP BY DATE_TRUNC('week', ss.created_at), ss.standard_id
163
- ),
164
- weekly_devs AS (
165
- SELECT
166
- DATE_TRUNC('week', s.started_at) as week,
167
- COUNT(DISTINCT s.email_address) as active_devs
168
- FROM rapport.sessions s
169
- JOIN rapport.projects p ON s.project_id = p.project_id
170
- WHERE p.company_id = $1
171
- AND s.started_at >= $2
172
- AND s.email_address IS NOT NULL
173
- GROUP BY DATE_TRUNC('week', s.started_at)
174
- )
175
- SELECT
176
- wa.week,
177
- ROUND(AVG(
178
- CASE WHEN wd.active_devs > 0
179
- THEN (wa.devs_using::numeric / wd.active_devs) * 100
180
- ELSE 0
181
- END
182
- )) as score,
183
- COUNT(DISTINCT wa.standard_id) as standards_count,
184
- MAX(wd.active_devs) as developer_count
185
- FROM weekly_adoption wa
186
- JOIN weekly_devs wd ON wa.week = wd.week
187
- GROUP BY wa.week
188
- ORDER BY wa.week
189
- `, [userCompanyId, periodStart]);
190
-
191
- convergenceTrend = trendResult.rows.map(row => ({
192
- week: row.week,
193
- score: parseInt(row.score) || 0,
194
- standards_count: parseInt(row.standards_count),
195
- developer_count: parseInt(row.developer_count)
196
- }));
197
- } catch (e) {
198
- console.log('[Convergence] Trend query failed:', e.message);
199
- }
200
-
201
- // Determine trend direction
202
- let trend = 'stable';
203
- if (convergenceTrend.length >= 2) {
204
- const recent = convergenceTrend[convergenceTrend.length - 1].score;
205
- const earlier = convergenceTrend[0].score;
206
- if (recent > earlier + 5) trend = 'improving';
207
- else if (recent < earlier - 5) trend = 'declining';
208
- }
209
-
210
- return createSuccessResponse({
211
- convergence_score: convergenceScore,
212
- trend,
213
- developer_count: activeDevelopers.length,
214
- standards_in_use: byStandard.length,
215
- by_standard: byStandard,
216
- by_developer: byDeveloper,
217
- convergence_trend: convergenceTrend
218
- }, 'Convergence analytics retrieved');
219
- }
220
-
221
- exports.handler = wrapHandler(getConvergence);
222
-
223
- function parsePeriod(period) {
224
- const match = period.match(/^(\d+)([dwm])$/);
225
- if (!match) return 30;
226
-
227
- const [, num, unit] = match;
228
- const n = parseInt(num);
229
-
230
- switch (unit) {
231
- case 'd': return n;
232
- case 'w': return n * 7;
233
- case 'm': return n * 30;
234
- default: return 30;
235
- }
236
- }