@equilateral_ai/mindmeld 3.3.1 → 3.5.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 (72) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-end.js +112 -3
  4. package/hooks/session-start.js +635 -41
  5. package/hooks/subagent-start.js +150 -0
  6. package/hooks/subagent-stop.js +184 -0
  7. package/package.json +8 -7
  8. package/scripts/init-project.js +74 -33
  9. package/scripts/mcp-bridge.js +220 -0
  10. package/src/core/CorrelationAnalyzer.js +157 -0
  11. package/src/core/LLMPatternDetector.js +198 -0
  12. package/src/core/RelevanceDetector.js +123 -36
  13. package/src/core/StandardsIngestion.js +119 -18
  14. package/src/handlers/activity/activityGetMe.js +1 -1
  15. package/src/handlers/activity/activityGetTeam.js +100 -55
  16. package/src/handlers/admin/adminSetup.js +216 -0
  17. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  18. package/src/handlers/alerts/alertsGet.js +11 -11
  19. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  20. package/src/handlers/analytics/coachingGet.js +11 -11
  21. package/src/handlers/analytics/convergenceGet.js +236 -0
  22. package/src/handlers/analytics/developerScoreGet.js +41 -111
  23. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  24. package/src/handlers/company/companyUsersDelete.js +141 -0
  25. package/src/handlers/company/companyUsersGet.js +90 -0
  26. package/src/handlers/company/companyUsersPost.js +267 -0
  27. package/src/handlers/company/companyUsersPut.js +76 -0
  28. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  29. package/src/handlers/correlations/correlationsGet.js +8 -8
  30. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  31. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  32. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  33. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  34. package/src/handlers/github/githubConnectionStatus.js +1 -1
  35. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  36. package/src/handlers/github/githubPatternsReview.js +7 -36
  37. package/src/handlers/health/healthGet.js +55 -0
  38. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  39. package/src/handlers/helpers/mindmeldMcpCore.js +594 -0
  40. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  41. package/src/handlers/mcp/mcpHandler.js +569 -0
  42. package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
  43. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -0
  44. package/src/handlers/notifications/sendNotification.js +18 -18
  45. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  46. package/src/handlers/projects/projectCreate.js +124 -10
  47. package/src/handlers/projects/projectDelete.js +4 -4
  48. package/src/handlers/projects/projectGet.js +8 -8
  49. package/src/handlers/projects/projectUpdate.js +4 -4
  50. package/src/handlers/reports/aiLeverage.js +34 -30
  51. package/src/handlers/reports/engineeringInvestment.js +16 -16
  52. package/src/handlers/reports/riskForecast.js +41 -21
  53. package/src/handlers/reports/standardsRoi.js +101 -9
  54. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  55. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  56. package/src/handlers/standards/discoveriesGet.js +93 -0
  57. package/src/handlers/standards/projectStandardsGet.js +2 -2
  58. package/src/handlers/standards/projectStandardsPut.js +2 -2
  59. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  60. package/src/handlers/standards/standardsTransition.js +112 -15
  61. package/src/handlers/stripe/billingPortalPost.js +1 -1
  62. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  63. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  64. package/src/handlers/stripe/webhookPost.js +42 -14
  65. package/src/handlers/user/apiTokenCreate.js +71 -0
  66. package/src/handlers/user/apiTokenList.js +64 -0
  67. package/src/handlers/user/userSplashGet.js +90 -73
  68. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  69. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  70. package/src/handlers/users/userGet.js +15 -11
  71. package/src/handlers/webhooks/githubWebhook.js +117 -125
  72. package/src/index.js +8 -5
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Company Users List Handler
3
+ * Lists all users (entitlements) for a company
4
+ *
5
+ * GET /api/company/users?company_id=xxx
6
+ * Auth: Cognito JWT required
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
10
+
11
+ /**
12
+ * List company users
13
+ * Requires member access to the company
14
+ */
15
+ async function listCompanyUsers({ queryStringParameters: queryParams = {}, requestContext }) {
16
+ try {
17
+ const Request_ID = requestContext.requestId;
18
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
19
+ const companyId = queryParams.company_id;
20
+
21
+ if (!email) {
22
+ return createErrorResponse(401, 'Authentication required');
23
+ }
24
+
25
+ if (!companyId) {
26
+ return createErrorResponse(400, 'company_id query parameter is required');
27
+ }
28
+
29
+ // Verify caller has access to this company
30
+ const accessQuery = `
31
+ SELECT admin, member
32
+ FROM rapport.user_entitlements
33
+ WHERE email_address = $1 AND company_id = $2
34
+ `;
35
+ const accessCheck = await executeQuery(accessQuery, [email, companyId]);
36
+
37
+ if (accessCheck.rowCount === 0) {
38
+ return createErrorResponse(403, 'You do not have access to this company');
39
+ }
40
+
41
+ // Get all users for the company
42
+ const query = `
43
+ SELECT
44
+ ue.email_address,
45
+ ue.admin,
46
+ ue.member,
47
+ ue.client_id,
48
+ ue.company_id,
49
+ ue.billing_type,
50
+ u.first_name,
51
+ u.last_name,
52
+ u.user_status,
53
+ u.create_date,
54
+ u.last_updated
55
+ FROM rapport.user_entitlements ue
56
+ LEFT JOIN rapport.users u ON u.email_address = ue.email_address
57
+ WHERE ue.company_id = $1
58
+ ORDER BY ue.admin DESC, u.create_date ASC
59
+ `;
60
+ const result = await executeQuery(query, [companyId]);
61
+
62
+ const users = result.rows.map(row => ({
63
+ email: row.email_address,
64
+ display_name: [row.first_name, row.last_name].filter(Boolean).join(' ') || null,
65
+ role: row.admin ? 'admin' : 'member',
66
+ status: row.user_status || 'active',
67
+ billing_type: row.billing_type || 'self_paid',
68
+ created_at: row.create_date,
69
+ last_active: row.last_updated,
70
+ client_id: row.client_id,
71
+ company_id: row.company_id,
72
+ }));
73
+
74
+ return createSuccessResponse(
75
+ { Records: users },
76
+ `Found ${users.length} user(s)`,
77
+ {
78
+ Total_Records: users.length,
79
+ Request_ID,
80
+ Timestamp: new Date().toISOString()
81
+ }
82
+ );
83
+
84
+ } catch (error) {
85
+ console.error('Handler Error:', error);
86
+ return handleError(error);
87
+ }
88
+ }
89
+
90
+ exports.handler = wrapHandler(listCompanyUsers);
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Company Users Invite Handler
3
+ * Invites a user to a company by creating an entitlement and sending an invite email
4
+ * Supports two billing modes: self_pays (invitee pays) or admin_pays (license on admin's subscription)
5
+ *
6
+ * POST /api/company/users
7
+ * Body: { company_id, email, role, billing }
8
+ * Auth: Cognito JWT required
9
+ */
10
+
11
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, getTierConfig } = require('./helpers');
12
+ const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
13
+ const Stripe = require('stripe');
14
+
15
+ const ses = new SESClient({ region: process.env.AWS_REGION || 'us-east-2' });
16
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
17
+
18
+ /**
19
+ * Invite user to company
20
+ * Requires admin access to the company
21
+ */
22
+ async function inviteCompanyUser({ body: requestBody = {}, requestContext }) {
23
+ try {
24
+ const Request_ID = requestContext.requestId;
25
+ const callerEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
26
+ const { company_id, email, role, billing = 'self_pays' } = requestBody;
27
+
28
+ if (!callerEmail) {
29
+ return createErrorResponse(401, 'Authentication required');
30
+ }
31
+
32
+ if (!company_id || !email) {
33
+ return createErrorResponse(400, 'company_id and email are required');
34
+ }
35
+
36
+ if (!['admin_pays', 'self_pays'].includes(billing)) {
37
+ return createErrorResponse(400, 'billing must be admin_pays or self_pays');
38
+ }
39
+
40
+ // Verify caller is admin of this company
41
+ const adminQuery = `
42
+ SELECT ue.admin, ue.client_id, u.first_name, u.last_name
43
+ FROM rapport.user_entitlements ue
44
+ LEFT JOIN rapport.users u ON u.email_address = ue.email_address
45
+ WHERE ue.email_address = $1 AND ue.company_id = $2
46
+ `;
47
+ const adminCheck = await executeQuery(adminQuery, [callerEmail, company_id]);
48
+
49
+ if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
50
+ return createErrorResponse(403, 'Admin access required to invite users');
51
+ }
52
+
53
+ const clientId = adminCheck.rows[0].client_id;
54
+ const inviterName = [adminCheck.rows[0].first_name, adminCheck.rows[0].last_name].filter(Boolean).join(' ') || callerEmail;
55
+
56
+ // Get client details for seat limits and Stripe info
57
+ const clientQuery = `
58
+ SELECT subscription_tier, seat_count, client_name, stripe_subscription_id, license_count
59
+ FROM rapport.clients WHERE client_id = $1
60
+ `;
61
+ const clientResult = await executeQuery(clientQuery, [clientId]);
62
+ const clientRecord = clientResult.rows[0];
63
+ const tierConfig = getTierConfig(clientRecord?.subscription_tier || 'free');
64
+ const maxCollaborators = tierConfig?.maxCollaborators;
65
+ const teamName = clientRecord?.client_name || 'their team';
66
+ const inviterTier = clientRecord?.subscription_tier || 'team';
67
+
68
+ // Check seat limits before allowing invite
69
+ if (maxCollaborators !== null) {
70
+ const countQuery = `
71
+ SELECT COUNT(*) as current_count
72
+ FROM rapport.user_entitlements WHERE company_id = $1
73
+ `;
74
+ const countResult = await executeQuery(countQuery, [company_id]);
75
+ const currentCount = parseInt(countResult.rows[0].current_count) || 0;
76
+
77
+ if (currentCount >= maxCollaborators) {
78
+ return createErrorResponse(403, `Seat limit reached (${currentCount}/${maxCollaborators}). Upgrade your plan to add more team members.`);
79
+ }
80
+ }
81
+
82
+ // For admin_pays, verify admin has an active Stripe subscription
83
+ if (billing === 'admin_pays' && !clientRecord?.stripe_subscription_id) {
84
+ return createErrorResponse(400, 'You need an active subscription to add licensed users. Subscribe first, then invite with "Add to my subscription".');
85
+ }
86
+
87
+ // Ensure invited user exists in users table (create pending record if not)
88
+ await executeQuery(`
89
+ INSERT INTO rapport.users (email_address, user_status, active, create_date)
90
+ VALUES ($1, 'Pending', true, NOW())
91
+ ON CONFLICT (email_address) DO NOTHING
92
+ `, [email]);
93
+
94
+ // Create entitlement for invited user
95
+ const isAdmin = role === 'admin';
96
+ const billingType = billing === 'admin_pays' ? 'admin_paid' : 'self_paid';
97
+ const insertQuery = `
98
+ INSERT INTO rapport.user_entitlements (
99
+ email_address, client_id, company_id, admin, member, billing_type
100
+ ) VALUES ($1, $2, $3, $4, true, $5)
101
+ ON CONFLICT (email_address, company_id) DO UPDATE SET
102
+ admin = EXCLUDED.admin,
103
+ member = true,
104
+ billing_type = EXCLUDED.billing_type
105
+ RETURNING email_address, admin, member, billing_type
106
+ `;
107
+ const result = await executeQuery(insertQuery, [email, clientId, company_id, isAdmin, billingType]);
108
+
109
+ // If admin_pays, update license count and Stripe subscription quantity
110
+ let stripeUpdated = false;
111
+ if (billing === 'admin_pays') {
112
+ // Increment license_count
113
+ const updateResult = await executeQuery(`
114
+ UPDATE rapport.clients
115
+ SET license_count = COALESCE(license_count, 1) + 1, last_updated = CURRENT_TIMESTAMP
116
+ WHERE client_id = $1
117
+ RETURNING license_count
118
+ `, [clientId]);
119
+ const newLicenseCount = updateResult.rows[0].license_count;
120
+
121
+ // Update Stripe subscription quantity
122
+ try {
123
+ const subscription = await stripe.subscriptions.retrieve(clientRecord.stripe_subscription_id);
124
+ const item = subscription.items.data[0];
125
+ if (item) {
126
+ await stripe.subscriptions.update(clientRecord.stripe_subscription_id, {
127
+ items: [{ id: item.id, quantity: newLicenseCount }],
128
+ proration_behavior: 'none'
129
+ });
130
+ stripeUpdated = true;
131
+ console.log(`[License] Updated Stripe quantity to ${newLicenseCount} for ${clientId}`);
132
+ }
133
+ } catch (stripeError) {
134
+ console.error('[License] Stripe update failed:', stripeError.message);
135
+ // Revert license_count since Stripe failed
136
+ await executeQuery(`
137
+ UPDATE rapport.clients
138
+ SET license_count = COALESCE(license_count, 2) - 1, last_updated = CURRENT_TIMESTAMP
139
+ WHERE client_id = $1
140
+ `, [clientId]);
141
+ return createErrorResponse(500, 'Failed to update subscription. Please try again or contact support.');
142
+ }
143
+ }
144
+
145
+ // Send invite email
146
+ const appUrl = process.env.APP_URL || 'https://app.mindmeld.dev';
147
+ // Admin-paid users don't need to go through checkout, so no ?tier= param
148
+ const signupUrl = billing === 'admin_pays'
149
+ ? `${appUrl}/signup`
150
+ : `${appUrl}/signup?tier=${inviterTier}`;
151
+ let emailSent = false;
152
+
153
+ // Customize email message based on billing type
154
+ const billingNote = billing === 'admin_pays'
155
+ ? 'Your subscription is covered — just sign up and start using MindMeld.'
156
+ : 'MindMeld brings intelligent standards and patterns directly into your AI coding sessions — helping your team write better code, faster.';
157
+
158
+ const emailParams = {
159
+ Source: process.env.EMAIL_FROM || 'noreply@mindmeld.dev',
160
+ Destination: {
161
+ ToAddresses: [email]
162
+ },
163
+ Message: {
164
+ Subject: {
165
+ Data: `${inviterName} invited you to join ${teamName} on MindMeld`,
166
+ Charset: 'UTF-8'
167
+ },
168
+ Body: {
169
+ Html: {
170
+ Data: `
171
+ <!DOCTYPE html>
172
+ <html>
173
+ <head>
174
+ <meta charset="utf-8">
175
+ <style>
176
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
177
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
178
+ .header { text-align: center; margin-bottom: 30px; }
179
+ .logo { font-size: 24px; font-weight: bold; color: #2563eb; }
180
+ .content { background: #f8fafc; border-radius: 8px; padding: 30px; margin-bottom: 20px; }
181
+ .button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500; }
182
+ .footer { text-align: center; color: #64748b; font-size: 14px; }
183
+ .role-badge { display: inline-block; background: #e0e7ff; color: #3730a3; padding: 4px 12px; border-radius: 20px; font-size: 14px; }
184
+ </style>
185
+ </head>
186
+ <body>
187
+ <div class="container">
188
+ <div class="header">
189
+ <div class="logo">MindMeld</div>
190
+ </div>
191
+ <div class="content">
192
+ <p>Hi there,</p>
193
+ <p><strong>${inviterName}</strong> has invited you to join <strong>${teamName}</strong> on MindMeld.</p>
194
+ <p>Your role: <span class="role-badge">${isAdmin ? 'Admin' : 'Member'}</span></p>
195
+ <p>${billingNote}</p>
196
+ <p style="margin-top: 30px;">
197
+ <a href="${signupUrl}" class="button">Get Started</a>
198
+ </p>
199
+ <p style="margin-top: 20px; font-size: 14px; color: #64748b;">
200
+ Already have an account? <a href="${appUrl}" style="color: #2563eb;">Sign in</a> — your team access will be ready.
201
+ </p>
202
+ </div>
203
+ <div class="footer">
204
+ <p>MindMeld - Intelligent standards for AI coding</p>
205
+ <p>Powered by Equilateral AI</p>
206
+ </div>
207
+ </div>
208
+ </body>
209
+ </html>
210
+ `,
211
+ Charset: 'UTF-8'
212
+ },
213
+ Text: {
214
+ Data: `${inviterName} has invited you to join ${teamName} on MindMeld.
215
+
216
+ Your role: ${isAdmin ? 'Admin' : 'Member'}
217
+
218
+ ${billingNote}
219
+
220
+ Get started: ${signupUrl}
221
+
222
+ Already have an account? Sign in at ${appUrl} — your team access will be ready.
223
+
224
+ ---
225
+ MindMeld - Intelligent standards for AI coding
226
+ Powered by Equilateral AI
227
+ `,
228
+ Charset: 'UTF-8'
229
+ }
230
+ }
231
+ }
232
+ };
233
+
234
+ try {
235
+ await ses.send(new SendEmailCommand(emailParams));
236
+ emailSent = true;
237
+ } catch (sesError) {
238
+ console.error('SES Error sending invite:', sesError);
239
+ }
240
+
241
+ const responseRecords = result.rows.map(r => ({
242
+ ...r,
243
+ email_sent: emailSent,
244
+ stripe_updated: stripeUpdated
245
+ }));
246
+
247
+ const message = billing === 'admin_pays'
248
+ ? `${email} added to your subscription (license ${stripeUpdated ? 'updated' : 'pending'})`
249
+ : emailSent ? `Invitation sent to ${email}` : `User ${email} added to team (email delivery failed — share the link manually)`;
250
+
251
+ return createSuccessResponse(
252
+ { Records: responseRecords },
253
+ message,
254
+ {
255
+ Total_Records: result.rowCount,
256
+ Request_ID,
257
+ Timestamp: new Date().toISOString()
258
+ }
259
+ );
260
+
261
+ } catch (error) {
262
+ console.error('Handler Error:', error);
263
+ return handleError(error);
264
+ }
265
+ }
266
+
267
+ exports.handler = wrapHandler(inviteCompanyUser);
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Company Users Update Handler
3
+ * Updates a user's role within a company
4
+ *
5
+ * PUT /api/company/users
6
+ * Body: { company_id, email, role }
7
+ * Auth: Cognito JWT required
8
+ */
9
+
10
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
11
+
12
+ /**
13
+ * Update user role in company
14
+ * Requires admin access to the company
15
+ */
16
+ async function updateCompanyUser({ body: requestBody = {}, requestContext }) {
17
+ try {
18
+ const Request_ID = requestContext.requestId;
19
+ const callerEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+ const { company_id, email, role } = requestBody;
21
+
22
+ if (!callerEmail) {
23
+ return createErrorResponse(401, 'Authentication required');
24
+ }
25
+
26
+ if (!company_id || !email || !role) {
27
+ return createErrorResponse(400, 'company_id, email, and role are required');
28
+ }
29
+
30
+ // Verify caller is admin of this company
31
+ const adminQuery = `
32
+ SELECT admin FROM rapport.user_entitlements
33
+ WHERE email_address = $1 AND company_id = $2
34
+ `;
35
+ const adminCheck = await executeQuery(adminQuery, [callerEmail, company_id]);
36
+
37
+ if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
38
+ return createErrorResponse(403, 'Admin access required to update user roles');
39
+ }
40
+
41
+ // Prevent self-demotion from admin
42
+ if (email === callerEmail && role !== 'admin') {
43
+ return createErrorResponse(400, 'Cannot remove your own admin role');
44
+ }
45
+
46
+ // Update the entitlement
47
+ const isAdmin = role === 'admin';
48
+ const updateQuery = `
49
+ UPDATE rapport.user_entitlements
50
+ SET admin = $1
51
+ WHERE email_address = $2 AND company_id = $3
52
+ RETURNING email_address, admin, member
53
+ `;
54
+ const result = await executeQuery(updateQuery, [isAdmin, email, company_id]);
55
+
56
+ if (result.rowCount === 0) {
57
+ return createErrorResponse(404, `User ${email} not found in this company`);
58
+ }
59
+
60
+ return createSuccessResponse(
61
+ { Records: result.rows },
62
+ `User ${email} role updated to ${role}`,
63
+ {
64
+ Total_Records: result.rowCount,
65
+ Request_ID,
66
+ Timestamp: new Date().toISOString()
67
+ }
68
+ );
69
+
70
+ } catch (error) {
71
+ console.error('Handler Error:', error);
72
+ return handleError(error);
73
+ }
74
+ }
75
+
76
+ exports.handler = wrapHandler(updateCompanyUser);
@@ -39,12 +39,12 @@ exports.handler = wrapHandler(async (event, context) => {
39
39
  if (!isSelf) {
40
40
  // Verify requesting user is admin in a shared company
41
41
  const accessResult = await executeQuery(`
42
- SELECT DISTINCT ue1."Company_ID"
43
- FROM "UserEntitlements" ue1
44
- JOIN "UserEntitlements" ue2 ON ue1."Company_ID" = ue2."Company_ID"
45
- WHERE ue1."Email_Address" = $1
46
- AND ue2."Email_Address" = $2
47
- AND (ue1."Admin" = true OR ue1."Manager" = true)
42
+ SELECT DISTINCT ue1.company_id
43
+ FROM rapport.user_entitlements ue1
44
+ JOIN rapport.user_entitlements ue2 ON ue1.company_id = ue2.company_id
45
+ WHERE ue1.email_address = $1
46
+ AND ue2.email_address = $2
47
+ AND (ue1.admin = true OR ue1.manager = true)
48
48
  `, [requestingEmail, targetEmail]);
49
49
 
50
50
  if (accessResult.rows.length === 0) {
@@ -55,12 +55,12 @@ exports.handler = wrapHandler(async (event, context) => {
55
55
  // Get developer info
56
56
  const developerResult = await executeQuery(`
57
57
  SELECT
58
- u."Email_Address" as email,
59
- u."User_Display_Name" as display_name,
60
- u."First_Name" as first_name,
61
- u."Last_Name" as last_name
62
- FROM "Users" u
63
- WHERE u."Email_Address" = $1
58
+ u.email_address as email,
59
+ CONCAT(u.first_name, ' ', u.last_name) as display_name,
60
+ u.first_name,
61
+ u.last_name
62
+ FROM rapport.users u
63
+ WHERE u.email_address = $1
64
64
  `, [targetEmail]);
65
65
 
66
66
  if (developerResult.rows.length === 0) {
@@ -28,9 +28,9 @@ exports.handler = wrapHandler(async (event, context) => {
28
28
 
29
29
  // Get user's company
30
30
  const companyResult = await executeQuery(`
31
- SELECT ue."Company_ID"
32
- FROM "UserEntitlements" ue
33
- WHERE ue."Email_Address" = $1
31
+ SELECT ue.company_id
32
+ FROM rapport.user_entitlements ue
33
+ WHERE ue.email_address = $1
34
34
  LIMIT 1
35
35
  `, [email]);
36
36
 
@@ -38,7 +38,7 @@ exports.handler = wrapHandler(async (event, context) => {
38
38
  return createErrorResponse(403, 'User not associated with any company');
39
39
  }
40
40
 
41
- const companyId = companyResult.rows[0].Company_ID;
41
+ const companyId = companyResult.rows[0].company_id;
42
42
 
43
43
  // Initialize analyzer
44
44
  const analyzer = new CorrelationAnalyzer();
@@ -83,11 +83,11 @@ exports.handler = wrapHandler(async (event, context) => {
83
83
  */
84
84
  async function checkIsAdmin(email, companyId) {
85
85
  const result = await executeQuery(`
86
- SELECT "Admin", "Manager"
87
- FROM "UserEntitlements"
88
- WHERE "Email_Address" = $1 AND "Company_ID" = $2
86
+ SELECT admin, manager
87
+ FROM rapport.user_entitlements
88
+ WHERE email_address = $1 AND company_id = $2
89
89
  `, [email, companyId]);
90
90
 
91
91
  if (result.rows.length === 0) return false;
92
- return result.rows[0].Admin || result.rows[0].Manager;
92
+ return result.rows[0].admin || result.rows[0].manager;
93
93
  }
@@ -83,7 +83,7 @@ async function getDeveloperBreakdown(projectId, lookbackDays) {
83
83
  const query = `
84
84
  SELECT
85
85
  sc.email_address,
86
- u."User_Display_Name" as display_name,
86
+ CONCAT(u.first_name, ' ', u.last_name) as display_name,
87
87
  COUNT(*) as total_sessions,
88
88
  COUNT(*) FILTER (WHERE sc.has_commits = true) as productive_sessions,
89
89
  ROUND(
@@ -97,10 +97,10 @@ async function getDeveloperBreakdown(projectId, lookbackDays) {
97
97
  ROUND(AVG(sc.session_duration_seconds) / 60, 0) as avg_session_minutes,
98
98
  MAX(sc.session_started_at) as last_session
99
99
  FROM rapport.session_correlations sc
100
- JOIN "Users" u ON sc.email_address = u."Email_Address"
100
+ JOIN rapport.users u ON sc.email_address = u.email_address
101
101
  WHERE sc.project_id = $1
102
102
  AND sc.session_started_at > NOW() - $2 * INTERVAL '1 day'
103
- GROUP BY sc.email_address, u."User_Display_Name"
103
+ GROUP BY sc.email_address, u.first_name, u.last_name
104
104
  ORDER BY total_commits DESC NULLS LAST
105
105
  `;
106
106
 
@@ -128,7 +128,7 @@ async function getRecentCorrelations(projectId, limit) {
128
128
  SELECT
129
129
  sc.session_id,
130
130
  sc.email_address,
131
- u."User_Display_Name" as display_name,
131
+ CONCAT(u.first_name, ' ', u.last_name) as display_name,
132
132
  sc.session_started_at,
133
133
  sc.session_duration_seconds,
134
134
  sc.has_commits,
@@ -138,7 +138,7 @@ async function getRecentCorrelations(projectId, limit) {
138
138
  sc.correlation_type,
139
139
  sc.correlation_score
140
140
  FROM rapport.session_correlations sc
141
- JOIN "Users" u ON sc.email_address = u."Email_Address"
141
+ JOIN rapport.users u ON sc.email_address = u.email_address
142
142
  WHERE sc.project_id = $1
143
143
  ORDER BY sc.session_started_at DESC
144
144
  LIMIT $2