@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,150 @@
1
+ /**
2
+ * Cognito Post-Confirmation Handler
3
+ * Creates user record and personal workspace after email verification
4
+ *
5
+ * Triggered by: Cognito User Pool post-confirmation trigger
6
+ * Following: cognito_authentication_standards.md
7
+ */
8
+
9
+ const { executeQuery } = require('./helpers');
10
+
11
+ /**
12
+ * Handle post-confirmation trigger
13
+ * Creates user, personal client, personal company, and entitlement
14
+ */
15
+ async function handler(event, context) {
16
+ console.log('Post-confirmation trigger:', JSON.stringify(event, null, 2));
17
+
18
+ // Only process SignUp confirmations
19
+ if (event.triggerSource !== 'PostConfirmation_ConfirmSignUp') {
20
+ console.log('Skipping trigger source:', event.triggerSource);
21
+ return event;
22
+ }
23
+
24
+ const { userName, request } = event;
25
+ const { userAttributes } = request;
26
+
27
+ const email = userAttributes.email;
28
+ const cognitoSub = userAttributes.sub;
29
+ const firstName = userAttributes.given_name || '';
30
+ const lastName = userAttributes.family_name || '';
31
+
32
+ try {
33
+ await executeQuery('BEGIN');
34
+
35
+ try {
36
+ // 1. Create user record
37
+ const userQuery = `
38
+ INSERT INTO rapport.users (
39
+ email_address,
40
+ cognito_sub,
41
+ first_name,
42
+ last_name,
43
+ client_id,
44
+ user_status,
45
+ active
46
+ )
47
+ VALUES ($1, $2, $3, $4, $5, 'Active', true)
48
+ ON CONFLICT (email_address) DO UPDATE SET
49
+ cognito_sub = EXCLUDED.cognito_sub,
50
+ first_name = EXCLUDED.first_name,
51
+ last_name = EXCLUDED.last_name,
52
+ last_updated = CURRENT_TIMESTAMP
53
+ RETURNING email_address
54
+ `;
55
+
56
+ // Personal client ID based on email (sanitized)
57
+ const personalClientId = `personal_${email.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}`;
58
+
59
+ await executeQuery(userQuery, [
60
+ email,
61
+ cognitoSub,
62
+ firstName,
63
+ lastName,
64
+ personalClientId
65
+ ]);
66
+
67
+ console.log('User created/updated:', email);
68
+
69
+ // 2. Create personal client (free tier)
70
+ const clientQuery = `
71
+ INSERT INTO rapport.clients (
72
+ client_id,
73
+ client_name,
74
+ client_type,
75
+ client_status,
76
+ subscription_tier,
77
+ subscription_status
78
+ )
79
+ VALUES ($1, $2, 'PERSONAL', 'Active', 'free', 'active')
80
+ ON CONFLICT (client_id) DO NOTHING
81
+ RETURNING client_id
82
+ `;
83
+
84
+ const clientName = firstName && lastName
85
+ ? `${firstName} ${lastName}'s Workspace`
86
+ : `${email.split('@')[0]}'s Workspace`;
87
+
88
+ await executeQuery(clientQuery, [personalClientId, clientName]);
89
+
90
+ console.log('Personal client created:', personalClientId);
91
+
92
+ // 3. Create personal company
93
+ const companyId = `${personalClientId}_main`;
94
+ const companyQuery = `
95
+ INSERT INTO rapport.companies (
96
+ company_id,
97
+ client_id,
98
+ company_name,
99
+ company_status
100
+ )
101
+ VALUES ($1, $2, $3, 'Active')
102
+ ON CONFLICT (company_id) DO NOTHING
103
+ RETURNING company_id
104
+ `;
105
+
106
+ await executeQuery(companyQuery, [
107
+ companyId,
108
+ personalClientId,
109
+ 'Personal Projects'
110
+ ]);
111
+
112
+ console.log('Personal company created:', companyId);
113
+
114
+ // 4. Create admin entitlement
115
+ const entitlementQuery = `
116
+ INSERT INTO rapport.user_entitlements (
117
+ email_address,
118
+ client_id,
119
+ company_id,
120
+ admin,
121
+ member
122
+ )
123
+ VALUES ($1, $2, $3, true, true)
124
+ ON CONFLICT (email_address, company_id) DO NOTHING
125
+ `;
126
+
127
+ await executeQuery(entitlementQuery, [email, personalClientId, companyId]);
128
+
129
+ console.log('Entitlement created for:', email);
130
+
131
+ await executeQuery('COMMIT');
132
+
133
+ console.log('Post-confirmation complete for:', email);
134
+
135
+ } catch (error) {
136
+ await executeQuery('ROLLBACK');
137
+ throw error;
138
+ }
139
+
140
+ } catch (error) {
141
+ console.error('Post-confirmation error:', error);
142
+ // Don't throw - this would prevent user creation in Cognito
143
+ // Log the error and continue
144
+ }
145
+
146
+ // Must return the event for Cognito to proceed
147
+ return event;
148
+ }
149
+
150
+ module.exports = { handler };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * User Entitlements Get Handler
3
+ * Retrieves user's company/project access rights
4
+ *
5
+ * GET /api/users/entitlements
6
+ * Auth: Cognito JWT required
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
10
+
11
+ /**
12
+ * Get current user's entitlements
13
+ */
14
+ async function getEntitlements({ requestContext }) {
15
+ try {
16
+ const Request_ID = requestContext.requestId;
17
+ // REST API: requestContext.authorizer.claims.email
18
+ // HTTP API: requestContext.authorizer.jwt.claims.email
19
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+
21
+ if (!email) {
22
+ return createErrorResponse(401, 'Authentication required');
23
+ }
24
+
25
+ // Get all entitlements with company and client details
26
+ const query = `
27
+ SELECT
28
+ ue.email_address,
29
+ ue.client_id,
30
+ c.client_name,
31
+ c.subscription_tier,
32
+ ue.company_id,
33
+ co.company_name,
34
+ ue.admin,
35
+ ue.member,
36
+ ue.create_date
37
+ FROM rapport.user_entitlements ue
38
+ JOIN rapport.clients c ON ue.client_id = c.client_id
39
+ JOIN rapport.companies co ON ue.company_id = co.company_id
40
+ WHERE ue.email_address = $1
41
+ AND c.active = true
42
+ AND co.active = true
43
+ ORDER BY c.client_name, co.company_name
44
+ `;
45
+
46
+ const result = await executeQuery(query, [email]);
47
+
48
+ // Group by client
49
+ const clientMap = {};
50
+ for (const row of result.rows) {
51
+ if (!clientMap[row.client_id]) {
52
+ clientMap[row.client_id] = {
53
+ client_id: row.client_id,
54
+ client_name: row.client_name,
55
+ subscription_tier: row.subscription_tier,
56
+ companies: []
57
+ };
58
+ }
59
+ clientMap[row.client_id].companies.push({
60
+ company_id: row.company_id,
61
+ company_name: row.company_name,
62
+ admin: row.admin,
63
+ member: row.member,
64
+ since: row.create_date
65
+ });
66
+ }
67
+
68
+ const clients = Object.values(clientMap);
69
+
70
+ return createSuccessResponse(
71
+ {
72
+ Records: clients
73
+ },
74
+ 'Entitlements retrieved',
75
+ {
76
+ Total_Records: clients.length,
77
+ Total_Companies: result.rowCount,
78
+ Request_ID,
79
+ Timestamp: new Date().toISOString()
80
+ }
81
+ );
82
+
83
+ } catch (error) {
84
+ console.error('Handler Error:', error);
85
+ return handleError(error);
86
+ }
87
+ }
88
+
89
+ exports.handler = wrapHandler(getEntitlements);
@@ -0,0 +1,114 @@
1
+ /**
2
+ * User Get Handler
3
+ * Retrieves user profile and subscription info
4
+ *
5
+ * GET /api/users/me
6
+ * Auth: Cognito JWT required
7
+ */
8
+
9
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, getTierConfig } = require('./helpers');
10
+
11
+ /**
12
+ * Get current user's profile
13
+ */
14
+ async function getUser({ requestContext }) {
15
+ try {
16
+ const Request_ID = requestContext.requestId;
17
+ // REST API: requestContext.authorizer.claims.email
18
+ // HTTP API: requestContext.authorizer.jwt.claims.email
19
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+
21
+ if (!email) {
22
+ return createErrorResponse(401, 'Authentication required');
23
+ }
24
+
25
+ // Get user with their primary client subscription
26
+ const query = `
27
+ SELECT
28
+ u.email_address,
29
+ u.first_name,
30
+ u.last_name,
31
+ u.client_id,
32
+ u.user_status,
33
+ u.create_date,
34
+ c.client_name,
35
+ c.subscription_tier,
36
+ c.subscription_status,
37
+ c.subscription_ends_at,
38
+ c.stripe_customer_id
39
+ FROM rapport.users u
40
+ LEFT JOIN rapport.clients c ON u.client_id = c.client_id
41
+ WHERE u.email_address = $1
42
+ AND u.active = true
43
+ `;
44
+
45
+ const result = await executeQuery(query, [email]);
46
+
47
+ if (result.rowCount === 0) {
48
+ return createErrorResponse(404, 'User not found');
49
+ }
50
+
51
+ const user = result.rows[0];
52
+ const tierConfig = getTierConfig(user.subscription_tier || 'free');
53
+
54
+ // Get usage counts
55
+ const usageQuery = `
56
+ SELECT
57
+ (SELECT COUNT(*) FROM rapport.user_entitlements WHERE client_id = $1) as collaborators,
58
+ (SELECT COUNT(*) FROM rapport.projects p
59
+ JOIN rapport.companies co ON p.company_id = co.company_id
60
+ WHERE co.client_id = $1 AND p.archived = false) as projects,
61
+ (SELECT COUNT(*) FROM rapport.invariants i
62
+ JOIN rapport.projects p ON i.project_id = p.project_id
63
+ JOIN rapport.companies co ON p.company_id = co.company_id
64
+ WHERE co.client_id = $1) as invariants
65
+ `;
66
+
67
+ const usageResult = await executeQuery(usageQuery, [user.client_id]);
68
+ const usage = usageResult.rows[0];
69
+
70
+ return createSuccessResponse(
71
+ {
72
+ Records: [{
73
+ email_address: user.email_address,
74
+ first_name: user.first_name,
75
+ last_name: user.last_name,
76
+ client_id: user.client_id,
77
+ client_name: user.client_name,
78
+ user_status: user.user_status,
79
+ member_since: user.create_date,
80
+ subscription: {
81
+ tier: user.subscription_tier || 'free',
82
+ tier_name: tierConfig?.displayName || 'Free',
83
+ status: user.subscription_status || 'active',
84
+ ends_at: user.subscription_ends_at,
85
+ has_stripe: !!user.stripe_customer_id,
86
+ features: tierConfig?.features || []
87
+ },
88
+ usage: {
89
+ collaborators: parseInt(usage.collaborators) || 0,
90
+ projects: parseInt(usage.projects) || 0,
91
+ invariants: parseInt(usage.invariants) || 0
92
+ },
93
+ limits: {
94
+ max_collaborators: tierConfig?.maxCollaborators || 1,
95
+ max_projects: tierConfig?.maxProjects || 3,
96
+ max_invariants: tierConfig?.maxInvariants || 10
97
+ }
98
+ }]
99
+ },
100
+ 'User profile retrieved',
101
+ {
102
+ Total_Records: 1,
103
+ Request_ID,
104
+ Timestamp: new Date().toISOString()
105
+ }
106
+ );
107
+
108
+ } catch (error) {
109
+ console.error('Handler Error:', error);
110
+ return handleError(error);
111
+ }
112
+ }
113
+
114
+ exports.handler = wrapHandler(getUser);
@@ -0,0 +1,223 @@
1
+ /**
2
+ * GitHub Webhook Handler
3
+ * Receives push and pull_request events to track commits and PRs
4
+ *
5
+ * POST /api/webhooks/github
6
+ * Auth: GitHub webhook signature verification (no Cognito)
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
+
12
+ exports.handler = wrapHandler(async ({ body, headers }) => {
13
+ // Verify webhook signature
14
+ const signature = headers['x-hub-signature-256'] || headers['X-Hub-Signature-256'];
15
+ const secret = process.env.GITHUB_WEBHOOK_SECRET;
16
+
17
+ if (!secret) {
18
+ console.error('GITHUB_WEBHOOK_SECRET not configured');
19
+ return createErrorResponse(500, 'Webhook not configured');
20
+ }
21
+
22
+ if (!verifySignature(JSON.stringify(body), signature, secret)) {
23
+ return createErrorResponse(401, 'Invalid signature');
24
+ }
25
+
26
+ const eventType = headers['x-github-event'] || headers['X-GitHub-Event'];
27
+
28
+ if (eventType === 'push') {
29
+ await handlePushEvent(body);
30
+ } else if (eventType === 'pull_request') {
31
+ await handlePREvent(body);
32
+ } else if (eventType === 'ping') {
33
+ // GitHub sends ping when webhook is first configured
34
+ return createSuccessResponse({ received: true }, 'Webhook configured');
35
+ }
36
+
37
+ return createSuccessResponse(
38
+ { received: true, event: eventType },
39
+ 'Webhook processed'
40
+ );
41
+ });
42
+
43
+ function verifySignature(payload, signature, secret) {
44
+ if (!signature) return false;
45
+
46
+ const expectedSignature = 'sha256=' + crypto
47
+ .createHmac('sha256', secret)
48
+ .update(payload)
49
+ .digest('hex');
50
+
51
+ return crypto.timingSafeEqual(
52
+ Buffer.from(signature),
53
+ Buffer.from(expectedSignature)
54
+ );
55
+ }
56
+
57
+ async function handlePushEvent(payload) {
58
+ const repoFullName = payload.repository?.full_name;
59
+ if (!repoFullName) return;
60
+
61
+ // Find project by repo URL
62
+ const projectResult = await executeQuery(`
63
+ SELECT project_id FROM rapport.projects
64
+ WHERE repo_url LIKE $1
65
+ `, [`%${repoFullName}%`]);
66
+
67
+ if (projectResult.rowCount === 0) {
68
+ console.log(`Project not found for repo: ${repoFullName}`);
69
+ return;
70
+ }
71
+
72
+ const projectId = projectResult.rows[0].project_id;
73
+ const branch = payload.ref?.replace('refs/heads/', '') || 'unknown';
74
+
75
+ for (const commit of (payload.commits || [])) {
76
+ await recordCommit(commit, projectId, branch);
77
+ }
78
+ }
79
+
80
+ async function recordCommit(commit, projectId, branch) {
81
+ const authorEmail = commit.author?.email;
82
+ if (!authorEmail) return;
83
+
84
+ // Find user by email
85
+ const userResult = await executeQuery(`
86
+ SELECT "Email_Address" FROM "Users"
87
+ WHERE "Email_Address" = $1 AND active = true
88
+ `, [authorEmail]);
89
+
90
+ if (userResult.rowCount === 0) {
91
+ console.log(`User not found for email: ${authorEmail}`);
92
+ return;
93
+ }
94
+
95
+ const email = userResult.rows[0].Email_Address;
96
+
97
+ // Find recent session for correlation
98
+ const sessionResult = await executeQuery(`
99
+ SELECT session_id, ended_at, started_at
100
+ FROM rapport.sessions
101
+ WHERE email_address = $1
102
+ AND project_id = $2
103
+ ORDER BY started_at DESC
104
+ LIMIT 1
105
+ `, [email, projectId]);
106
+
107
+ let sessionId = null;
108
+ let withinSession = false;
109
+ let timeSinceSession = null;
110
+
111
+ if (sessionResult.rowCount > 0) {
112
+ const session = sessionResult.rows[0];
113
+ const commitTime = new Date(commit.timestamp);
114
+ const sessionEnd = session.ended_at ? new Date(session.ended_at) : new Date();
115
+ const sessionStart = new Date(session.started_at);
116
+
117
+ // Commit within session if after start and before/during end
118
+ withinSession = commitTime >= sessionStart && (!session.ended_at || commitTime <= sessionEnd);
119
+
120
+ if (!withinSession && session.ended_at) {
121
+ timeSinceSession = Math.round((commitTime - sessionEnd) / (1000 * 60));
122
+ }
123
+
124
+ sessionId = session.session_id;
125
+ }
126
+
127
+ // Insert commit
128
+ await executeQuery(`
129
+ INSERT INTO rapport.commits (
130
+ commit_hash, project_id, email_address, branch, message,
131
+ committed_at, files_changed,
132
+ session_id, within_session, time_since_session_minutes
133
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
134
+ ON CONFLICT (commit_hash) DO NOTHING
135
+ `, [
136
+ commit.id,
137
+ projectId,
138
+ email,
139
+ branch,
140
+ commit.message?.substring(0, 500), // Truncate long messages
141
+ commit.timestamp,
142
+ (commit.added?.length || 0) + (commit.modified?.length || 0) + (commit.removed?.length || 0),
143
+ sessionId,
144
+ withinSession,
145
+ timeSinceSession
146
+ ]);
147
+
148
+ console.log(`Recorded commit ${commit.id} for ${email}`);
149
+ }
150
+
151
+ async function handlePREvent(payload) {
152
+ const pr = payload.pull_request;
153
+ const repoFullName = payload.repository?.full_name;
154
+ if (!pr || !repoFullName) return;
155
+
156
+ // Find project by repo URL
157
+ const projectResult = await executeQuery(`
158
+ SELECT project_id FROM rapport.projects
159
+ WHERE repo_url LIKE $1
160
+ `, [`%${repoFullName}%`]);
161
+
162
+ if (projectResult.rowCount === 0) {
163
+ console.log(`Project not found for repo: ${repoFullName}`);
164
+ return;
165
+ }
166
+
167
+ const projectId = projectResult.rows[0].project_id;
168
+ const prId = `github:${repoFullName}:${pr.number}`;
169
+
170
+ // Try to find user by email or login
171
+ const authorEmail = pr.user?.email || `${pr.user?.login}@users.noreply.github.com`;
172
+ const userResult = await executeQuery(`
173
+ SELECT "Email_Address" FROM "Users"
174
+ WHERE "Email_Address" = $1 AND active = true
175
+ `, [authorEmail]);
176
+
177
+ if (userResult.rowCount === 0) {
178
+ console.log(`User not found for: ${authorEmail}`);
179
+ return;
180
+ }
181
+
182
+ const email = userResult.rows[0].Email_Address;
183
+
184
+ // Calculate review time if merged
185
+ let reviewTimeHours = null;
186
+ if (pr.merged_at && pr.created_at) {
187
+ const created = new Date(pr.created_at);
188
+ const merged = new Date(pr.merged_at);
189
+ reviewTimeHours = (merged - created) / (1000 * 60 * 60);
190
+ }
191
+
192
+ // Upsert PR
193
+ await executeQuery(`
194
+ INSERT INTO rapport.pull_requests (
195
+ pr_id, project_id, email_address, title, branch, base_branch,
196
+ status, opened_at, merged_at, closed_at,
197
+ commits_count, files_changed, additions, deletions, review_time_hours
198
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
199
+ ON CONFLICT (pr_id) DO UPDATE SET
200
+ status = EXCLUDED.status,
201
+ merged_at = EXCLUDED.merged_at,
202
+ closed_at = EXCLUDED.closed_at,
203
+ review_time_hours = EXCLUDED.review_time_hours
204
+ `, [
205
+ prId,
206
+ projectId,
207
+ email,
208
+ pr.title?.substring(0, 500),
209
+ pr.head?.ref,
210
+ pr.base?.ref,
211
+ pr.merged ? 'merged' : pr.state,
212
+ pr.created_at,
213
+ pr.merged_at,
214
+ pr.closed_at,
215
+ pr.commits,
216
+ pr.changed_files,
217
+ pr.additions,
218
+ pr.deletions,
219
+ reviewTimeHours
220
+ ]);
221
+
222
+ console.log(`Recorded PR ${prId} for ${email}`);
223
+ }