@equilateral_ai/mindmeld 3.3.0 → 3.4.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 (69) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-start.js +636 -42
  4. package/hooks/subagent-start.js +150 -0
  5. package/hooks/subagent-stop.js +184 -0
  6. package/package.json +8 -7
  7. package/scripts/init-project.js +74 -33
  8. package/scripts/mcp-bridge.js +220 -0
  9. package/src/core/CorrelationAnalyzer.js +157 -0
  10. package/src/core/LLMPatternDetector.js +198 -0
  11. package/src/core/RelevanceDetector.js +123 -36
  12. package/src/core/StandardsIngestion.js +119 -18
  13. package/src/handlers/activity/activityGetMe.js +1 -1
  14. package/src/handlers/activity/activityGetTeam.js +100 -55
  15. package/src/handlers/admin/adminSetup.js +216 -0
  16. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  17. package/src/handlers/alerts/alertsGet.js +11 -11
  18. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  19. package/src/handlers/analytics/coachingGet.js +11 -11
  20. package/src/handlers/analytics/convergenceGet.js +236 -0
  21. package/src/handlers/analytics/developerScoreGet.js +41 -111
  22. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  23. package/src/handlers/company/companyUsersDelete.js +141 -0
  24. package/src/handlers/company/companyUsersGet.js +90 -0
  25. package/src/handlers/company/companyUsersPost.js +267 -0
  26. package/src/handlers/company/companyUsersPut.js +76 -0
  27. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  28. package/src/handlers/correlations/correlationsGet.js +8 -8
  29. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  30. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  31. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  32. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  33. package/src/handlers/github/githubConnectionStatus.js +1 -1
  34. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  35. package/src/handlers/github/githubPatternsReview.js +7 -36
  36. package/src/handlers/health/healthGet.js +55 -0
  37. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  38. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  39. package/src/handlers/mcp/mcpHandler.js +569 -0
  40. package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
  41. package/src/handlers/notifications/sendNotification.js +18 -18
  42. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  43. package/src/handlers/projects/projectCreate.js +124 -10
  44. package/src/handlers/projects/projectDelete.js +4 -4
  45. package/src/handlers/projects/projectGet.js +8 -8
  46. package/src/handlers/projects/projectUpdate.js +4 -4
  47. package/src/handlers/reports/aiLeverage.js +34 -30
  48. package/src/handlers/reports/engineeringInvestment.js +16 -16
  49. package/src/handlers/reports/riskForecast.js +41 -21
  50. package/src/handlers/reports/standardsRoi.js +101 -9
  51. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  52. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  53. package/src/handlers/standards/discoveriesGet.js +93 -0
  54. package/src/handlers/standards/projectStandardsGet.js +2 -2
  55. package/src/handlers/standards/projectStandardsPut.js +2 -2
  56. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  57. package/src/handlers/standards/standardsTransition.js +112 -15
  58. package/src/handlers/stripe/billingPortalPost.js +1 -1
  59. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  60. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  61. package/src/handlers/stripe/webhookPost.js +42 -14
  62. package/src/handlers/user/apiTokenCreate.js +71 -0
  63. package/src/handlers/user/apiTokenList.js +64 -0
  64. package/src/handlers/user/userSplashGet.js +90 -73
  65. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  66. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  67. package/src/handlers/users/userGet.js +12 -8
  68. package/src/handlers/webhooks/githubWebhook.js +117 -125
  69. package/src/index.js +46 -51
@@ -52,13 +52,13 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
52
52
  // Check authorization (only admins and managers can send notifications)
53
53
  const authCheck = await executeQuery(`
54
54
  SELECT
55
- ue."Admin",
56
- ue."Manager",
57
- u."Super_Admin",
58
- ue."Company_ID"
59
- FROM "UserEntitlements" ue
60
- JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
61
- WHERE ue."Email_Address" = $1
55
+ ue.admin,
56
+ ue.manager,
57
+ u.super_admin,
58
+ ue.company_id
59
+ FROM rapport.user_entitlements ue
60
+ JOIN rapport.users u ON ue.email_address = u.email_address
61
+ WHERE ue.email_address = $1
62
62
  `, [email]);
63
63
 
64
64
  if (authCheck.rowCount === 0) {
@@ -66,7 +66,7 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
66
66
  }
67
67
 
68
68
  const userRole = authCheck.rows[0];
69
- const isAuthorized = userRole.Super_Admin || userRole.Admin || userRole.Manager;
69
+ const isAuthorized = userRole.super_admin || userRole.admin || userRole.manager;
70
70
 
71
71
  if (!isAuthorized) {
72
72
  return createErrorResponse(403, 'Only admins and managers can send notifications');
@@ -89,18 +89,18 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
89
89
  } else if (recipients === 'all_admins') {
90
90
  // All company admins
91
91
  const admins = await executeQuery(`
92
- SELECT ue."Email_Address" as email_address
93
- FROM "UserEntitlements" ue
94
- WHERE ue."Company_ID" = $1 AND ue."Admin" = true
95
- `, [userRole.Company_ID]);
92
+ SELECT ue.email_address
93
+ FROM rapport.user_entitlements ue
94
+ WHERE ue.company_id = $1 AND ue.admin = true
95
+ `, [userRole.company_id]);
96
96
  recipientEmails = admins.rows.map(r => r.email_address);
97
97
  } else if (recipients === 'all_managers') {
98
98
  // All company managers
99
99
  const managers = await executeQuery(`
100
- SELECT ue."Email_Address" as email_address
101
- FROM "UserEntitlements" ue
102
- WHERE ue."Company_ID" = $1 AND (ue."Manager" = true OR ue."Admin" = true)
103
- `, [userRole.Company_ID]);
100
+ SELECT ue.email_address
101
+ FROM rapport.user_entitlements ue
102
+ WHERE ue.company_id = $1 AND (ue.manager = true OR ue.admin = true)
103
+ `, [userRole.company_id]);
104
104
  recipientEmails = managers.rows.map(r => r.email_address);
105
105
  } else {
106
106
  return createErrorResponse(400, 'Invalid recipients format');
@@ -118,8 +118,8 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
118
118
  // Get preferences for all recipients
119
119
  const prefsQuery = await executeQuery(`
120
120
  SELECT email_address, rapport.get_notification_preferences(email_address) as preferences
121
- FROM "Users"
122
- WHERE "Email_Address" = ANY($1)
121
+ FROM rapport.users
122
+ WHERE email_address = ANY($1)
123
123
  `, [recipientEmails]);
124
124
 
125
125
  const prefsMap = new Map(prefsQuery.rows.map(r => [r.email_address, r.preferences]));
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Pattern Evaluate Promotion Handler
3
+ * Checks if a pattern meets promotion thresholds for becoming a standard
4
+ *
5
+ * POST /api/patterns/evaluate-promotion
6
+ * Body: { pattern, project_id, evaluate_only }
7
+ *
8
+ * Called by: pre-compact.js hook (evaluateForPromotion method)
9
+ *
10
+ * Promotion Criteria:
11
+ * - handoff_count >= 10 (used 10+ times)
12
+ * - success_rate >= 0.70 (70% success)
13
+ * - developer_count >= 3 (used by 3+ developers)
14
+ */
15
+
16
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
17
+
18
+ /**
19
+ * Evaluate pattern for promotion to standard
20
+ */
21
+ async function evaluatePatternPromotion({ body: requestBody = {}, requestContext }) {
22
+ try {
23
+ const Request_ID = requestContext?.requestId || 'unknown';
24
+
25
+ const {
26
+ pattern,
27
+ project_id,
28
+ evaluate_only = true
29
+ } = requestBody;
30
+
31
+ // Validate required fields
32
+ if (!pattern || !pattern.element) {
33
+ return createErrorResponse(400, 'pattern.element is required');
34
+ }
35
+
36
+ // Generate pattern_id from element
37
+ const patternId = pattern.pattern_id || `pat_${pattern.element.toLowerCase().replace(/\s+/g, '_').substring(0, 50)}`;
38
+
39
+ // Look up pattern in rapport.patterns
40
+ const patternQuery = `
41
+ SELECT
42
+ p.pattern_id,
43
+ p.intent,
44
+ p.maturity,
45
+ p.handoff_count,
46
+ p.successful_handoffs,
47
+ p.failed_handoffs,
48
+ p.discovered_at,
49
+ p.last_used
50
+ FROM rapport.patterns p
51
+ WHERE p.pattern_id = $1
52
+ `;
53
+
54
+ const patternResult = await executeQuery(patternQuery, [patternId]);
55
+
56
+ if (patternResult.rows.length === 0) {
57
+ return createSuccessResponse(
58
+ {
59
+ Records: [{
60
+ pattern_id: patternId,
61
+ eligible: false,
62
+ reason: 'not_found',
63
+ message: 'Pattern not found in patterns table'
64
+ }]
65
+ },
66
+ 'Pattern not found',
67
+ { Total_Records: 0, Request_ID, Timestamp: new Date().toISOString() }
68
+ );
69
+ }
70
+
71
+ const pat = patternResult.rows[0];
72
+
73
+ // Query usage metrics
74
+ const metricsQuery = `
75
+ SELECT
76
+ COUNT(DISTINCT pu.email_address) as developer_count,
77
+ COUNT(DISTINCT pu.session_id) as session_count,
78
+ COUNT(*) FILTER (WHERE pu.success = true) as successes,
79
+ COUNT(*) as total_uses
80
+ FROM rapport.pattern_usage pu
81
+ WHERE pu.pattern_id = $1
82
+ `;
83
+
84
+ const metricsResult = await executeQuery(metricsQuery, [patternId]);
85
+ const metrics = metricsResult.rows[0];
86
+
87
+ const handoffCount = parseInt(pat.handoff_count) || 0;
88
+ const successRate = handoffCount > 0
89
+ ? (parseInt(pat.successful_handoffs) || 0) / handoffCount
90
+ : 0;
91
+ const developerCount = parseInt(metrics.developer_count) || 0;
92
+ const sessionCount = parseInt(metrics.session_count) || 0;
93
+
94
+ // Check promotion thresholds
95
+ const eligible =
96
+ handoffCount >= 10 &&
97
+ successRate >= 0.70 &&
98
+ developerCount >= 3;
99
+
100
+ const response = {
101
+ pattern_id: patternId,
102
+ eligible: eligible,
103
+ metrics: {
104
+ handoff_count: handoffCount,
105
+ success_rate: parseFloat(successRate.toFixed(3)),
106
+ developer_count: developerCount,
107
+ session_count: sessionCount,
108
+ maturity: pat.maturity,
109
+ success_correlation: parseFloat(successRate.toFixed(3))
110
+ },
111
+ thresholds: {
112
+ min_handoffs: 10,
113
+ min_success_rate: 0.70,
114
+ min_developers: 3
115
+ },
116
+ proposed_category: pattern.category || null
117
+ };
118
+
119
+ // If eligible and not evaluate_only, create curation candidate
120
+ if (eligible && !evaluate_only) {
121
+ const candidateQuery = `
122
+ INSERT INTO rapport.curation_candidates (
123
+ pattern_id,
124
+ proposed_category,
125
+ evidence,
126
+ status,
127
+ created_at
128
+ ) VALUES ($1, $2, $3, 'pending', NOW())
129
+ ON CONFLICT (pattern_id) WHERE status = 'pending'
130
+ DO UPDATE SET
131
+ evidence = EXCLUDED.evidence,
132
+ created_at = NOW()
133
+ RETURNING candidate_id
134
+ `;
135
+
136
+ const evidence = {
137
+ handoff_count: handoffCount,
138
+ success_rate: successRate,
139
+ developer_count: developerCount,
140
+ session_count: sessionCount,
141
+ maturity: pat.maturity,
142
+ evaluated_at: new Date().toISOString()
143
+ };
144
+
145
+ try {
146
+ const candidateResult = await executeQuery(candidateQuery, [
147
+ patternId,
148
+ pattern.category || 'uncategorized',
149
+ JSON.stringify(evidence)
150
+ ]);
151
+
152
+ if (candidateResult.rows.length > 0) {
153
+ response.candidate_id = candidateResult.rows[0].candidate_id;
154
+ }
155
+ } catch (candidateError) {
156
+ // Candidate creation failed — still return eligibility result
157
+ console.error('[patternEvaluatePromotionPost] Candidate creation failed:', candidateError.message);
158
+ }
159
+ }
160
+
161
+ return createSuccessResponse(
162
+ { Records: [response] },
163
+ eligible ? 'Pattern eligible for promotion' : 'Pattern does not meet promotion criteria',
164
+ { Total_Records: 1, Request_ID, Timestamp: new Date().toISOString() }
165
+ );
166
+
167
+ } catch (error) {
168
+ console.error('Handler Error:', error);
169
+ return handleError(error);
170
+ }
171
+ }
172
+
173
+ exports.handler = wrapHandler(evaluatePatternPromotion);
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, checkSubscriptionLimits } = require('./helpers');
10
+ const crypto = require('crypto');
11
+ const https = require('https');
10
12
 
11
13
  /**
12
14
  * Create project
@@ -27,24 +29,24 @@ async function createProject({ body: requestBody = {}, requestContext }) {
27
29
 
28
30
  // Check user has admin access to company
29
31
  const adminQuery = `
30
- SELECT ue."Admin", c."Company_Name"
31
- FROM "UserEntitlements" ue
32
- JOIN "Company" c ON ue."Company_ID" = c."Company_ID"
33
- WHERE ue."Email_Address" = $1
34
- AND ue."Company_ID" = $2
32
+ SELECT ue.admin, c.company_name
33
+ FROM rapport.user_entitlements ue
34
+ JOIN rapport.companies c ON ue.company_id = c.company_id
35
+ WHERE ue.email_address = $1
36
+ AND ue.company_id = $2
35
37
  `;
36
38
  const adminCheck = await executeQuery(adminQuery, [email, Company_ID]);
37
39
 
38
- if (adminCheck.rowCount === 0 || !adminCheck.rows[0].Admin) {
40
+ if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
39
41
  return createErrorResponse(403, 'Admin access required to create projects');
40
42
  }
41
43
 
42
44
  // Check project limit for subscription tier
43
45
  const clientQuery = `
44
46
  SELECT c.subscription_tier
45
- FROM "Client" c
46
- JOIN "UserEntitlements" ue ON c."Client_ID" = ue."Client_ID"
47
- WHERE ue."Email_Address" = $1 AND ue."Company_ID" = $2
47
+ FROM rapport.clients c
48
+ JOIN rapport.user_entitlements ue ON c.client_id = ue.client_id
49
+ WHERE ue.email_address = $1 AND ue.company_id = $2
48
50
  `;
49
51
  const clientResult = await executeQuery(clientQuery, [email, Company_ID]);
50
52
  const subscriptionTier = clientResult.rows[0]?.subscription_tier || 'free';
@@ -112,8 +114,18 @@ async function createProject({ body: requestBody = {}, requestContext }) {
112
114
  `;
113
115
  await executeQuery(collabQuery, [project_id, email]);
114
116
 
117
+ // Auto-create GitHub webhook if repo_url is a GitHub URL
118
+ let webhookCreated = false;
119
+ if (repo_url && repo_url.includes('github.com')) {
120
+ try {
121
+ webhookCreated = await createGitHubWebhook(email, repo_url, Company_ID);
122
+ } catch (webhookErr) {
123
+ console.warn('Webhook creation failed (non-blocking):', webhookErr.message);
124
+ }
125
+ }
126
+
115
127
  return createSuccessResponse(
116
- { Records: result.rows },
128
+ { Records: result.rows, webhook_created: webhookCreated },
117
129
  'Project created successfully',
118
130
  {
119
131
  Total_Records: result.rowCount,
@@ -131,4 +143,106 @@ async function createProject({ body: requestBody = {}, requestContext }) {
131
143
  }
132
144
  }
133
145
 
146
+ /**
147
+ * Create GitHub webhook on a repo when a project is connected
148
+ * Uses the customer's stored GitHub OAuth token and a per-repo secret
149
+ */
150
+ async function createGitHubWebhook(email, repoUrl, companyId) {
151
+ // Extract owner/repo from URL
152
+ const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
153
+ if (!match) {
154
+ console.log('Could not parse GitHub owner/repo from:', repoUrl);
155
+ return false;
156
+ }
157
+ const [, owner, repo] = match;
158
+
159
+ // Get the user's GitHub token
160
+ const connResult = await executeQuery(`
161
+ SELECT access_token_encrypted FROM rapport.github_connections
162
+ WHERE email_address = $1 AND revoked = FALSE
163
+ `, [email]);
164
+
165
+ if (connResult.rowCount === 0) {
166
+ console.log('No GitHub connection for', email);
167
+ return false;
168
+ }
169
+
170
+ const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
171
+ if (!encryptionKey) return false;
172
+
173
+ const accessToken = decryptToken(connResult.rows[0].access_token_encrypted, encryptionKey);
174
+
175
+ // Generate per-repo webhook secret
176
+ const webhookSecret = crypto.randomBytes(32).toString('hex');
177
+ const webhookUrl = process.env.WEBHOOK_URL || 'https://api.mindmeld.dev/api/webhooks/github';
178
+
179
+ // Create webhook via GitHub API
180
+ const payload = JSON.stringify({
181
+ name: 'web',
182
+ active: true,
183
+ events: ['push', 'pull_request'],
184
+ config: {
185
+ url: webhookUrl,
186
+ content_type: 'json',
187
+ secret: webhookSecret,
188
+ insecure_ssl: '0'
189
+ }
190
+ });
191
+
192
+ const response = await new Promise((resolve, reject) => {
193
+ const req = https.request({
194
+ hostname: 'api.github.com',
195
+ path: `/repos/${owner}/${repo}/hooks`,
196
+ method: 'POST',
197
+ headers: {
198
+ 'Authorization': `Bearer ${accessToken}`,
199
+ 'User-Agent': 'MindMeld-App',
200
+ 'Accept': 'application/vnd.github+json',
201
+ 'Content-Type': 'application/json',
202
+ 'Content-Length': Buffer.byteLength(payload)
203
+ }
204
+ }, (res) => {
205
+ let data = '';
206
+ res.on('data', chunk => data += chunk);
207
+ res.on('end', () => {
208
+ try { resolve({ statusCode: res.statusCode, body: JSON.parse(data) }); }
209
+ catch (e) { resolve({ statusCode: res.statusCode, body: data }); }
210
+ });
211
+ });
212
+ req.on('error', reject);
213
+ req.write(payload);
214
+ req.end();
215
+ });
216
+
217
+ if (response.statusCode !== 201) {
218
+ console.warn(`GitHub webhook creation returned ${response.statusCode}:`, JSON.stringify(response.body).substring(0, 200));
219
+ return false;
220
+ }
221
+
222
+ const githubWebhookId = response.body.id;
223
+
224
+ // Register repo in git_repositories with per-repo webhook secret
225
+ // Unique constraint is on (company_id, repo_url)
226
+ await executeQuery(`
227
+ INSERT INTO rapport.git_repositories (
228
+ repo_id, repo_name, repo_url, company_id, webhook_secret, created_at, updated_at
229
+ ) VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW())
230
+ ON CONFLICT (company_id, repo_url) DO UPDATE SET
231
+ webhook_secret = EXCLUDED.webhook_secret,
232
+ updated_at = NOW()
233
+ `, [`${owner}/${repo}`, repoUrl, companyId, webhookSecret]);
234
+
235
+ console.log(`Created GitHub webhook ${githubWebhookId} on ${owner}/${repo}`);
236
+ return true;
237
+ }
238
+
239
+ function decryptToken(encryptedData, key) {
240
+ const [ivHex, encrypted] = encryptedData.split(':');
241
+ const iv = Buffer.from(ivHex, 'hex');
242
+ const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
243
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
244
+ decrypted += decipher.final('utf8');
245
+ return decrypted;
246
+ }
247
+
134
248
  exports.handler = wrapHandler(createProject);
@@ -29,14 +29,14 @@ async function deleteProject({ queryStringParameters: queryParams = {}, requestC
29
29
  p.project_id,
30
30
  p.company_id,
31
31
  pc.role,
32
- ue."Admin" as company_admin
32
+ ue.admin as company_admin
33
33
  FROM rapport.projects p
34
34
  LEFT JOIN rapport.project_collaborators pc
35
35
  ON p.project_id = pc.project_id
36
36
  AND pc.email_address = $1
37
- LEFT JOIN "UserEntitlements" ue
38
- ON ue."Email_Address" = $1
39
- AND ue."Company_ID" = p.company_id
37
+ LEFT JOIN rapport.user_entitlements ue
38
+ ON ue.email_address = $1
39
+ AND ue.company_id = p.company_id
40
40
  WHERE p.project_id = $2
41
41
  `;
42
42
  const accessCheck = await executeQuery(accessQuery, [email, projectId]);
@@ -20,10 +20,10 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
20
20
 
21
21
  // Get user's company access
22
22
  const entitlementQuery = `
23
- SELECT ue."Company_ID", ue."Admin", c."Company_Name"
24
- FROM "UserEntitlements" ue
25
- JOIN "Company" c ON ue."Company_ID" = c."Company_ID"
26
- WHERE ue."Email_Address" = $1
23
+ SELECT ue.company_id, ue.admin, c.company_name
24
+ FROM rapport.user_entitlements ue
25
+ JOIN rapport.companies c ON ue.company_id = c.company_id
26
+ WHERE ue.email_address = $1
27
27
  `;
28
28
  const entitlements = await executeQuery(entitlementQuery, [email]);
29
29
 
@@ -31,7 +31,7 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
31
31
  return createErrorResponse(403, 'No company access');
32
32
  }
33
33
 
34
- const companyIds = entitlements.rows.map(e => e.Company_ID);
34
+ const companyIds = entitlements.rows.map(e => e.company_id);
35
35
 
36
36
  // Optional filter by specific company
37
37
  let targetCompanyIds = companyIds;
@@ -53,7 +53,7 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
53
53
  p.created_at,
54
54
  p.last_active,
55
55
  p.archived,
56
- c."Company_Name",
56
+ c.company_name,
57
57
  COUNT(DISTINCT pc.email_address) as collaborator_count,
58
58
  COALESCE(
59
59
  json_agg(
@@ -66,11 +66,11 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
66
66
  '[]'
67
67
  ) as collaborators
68
68
  FROM rapport.projects p
69
- JOIN "Company" c ON p.company_id = c."Company_ID"
69
+ JOIN rapport.companies c ON p.company_id = c.company_id
70
70
  LEFT JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
71
71
  WHERE p.company_id = ANY($1::varchar[])
72
72
  AND p.archived = false
73
- GROUP BY p.project_id, c."Company_Name"
73
+ GROUP BY p.project_id, c.company_name
74
74
  ORDER BY p.last_active DESC NULLS LAST, p.created_at DESC
75
75
  `;
76
76
 
@@ -30,14 +30,14 @@ async function updateProject({ body: requestBody = {}, requestContext }) {
30
30
  p.project_id,
31
31
  p.company_id,
32
32
  pc.role,
33
- ue."Admin" as company_admin
33
+ ue.admin as company_admin
34
34
  FROM rapport.projects p
35
35
  LEFT JOIN rapport.project_collaborators pc
36
36
  ON p.project_id = pc.project_id
37
37
  AND pc.email_address = $1
38
- LEFT JOIN "UserEntitlements" ue
39
- ON ue."Email_Address" = $1
40
- AND ue."Company_ID" = p.company_id
38
+ LEFT JOIN rapport.user_entitlements ue
39
+ ON ue.email_address = $1
40
+ AND ue.company_id = p.company_id
41
41
  WHERE p.project_id = $2
42
42
  `;
43
43
  const accessCheck = await executeQuery(accessQuery, [email, projectId]);
@@ -20,13 +20,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
20
20
  const period = params.period || '30d';
21
21
  const companyId = params.Company_ID;
22
22
 
23
- // Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
23
+ // Validate access - must be manager/admin
24
24
  const accessCheck = await executeQuery(`
25
- SELECT ue."Company_ID"
26
- FROM "UserEntitlements" ue
27
- WHERE ue."Email_Address" = $1
28
- AND (ue."Admin" = true OR ue."Manager" = true)
29
- ${companyId ? 'AND ue."Company_ID" = $2' : ''}
25
+ SELECT ue.company_id
26
+ FROM rapport.user_entitlements ue
27
+ WHERE ue.email_address = $1
28
+ AND (ue.admin = true OR ue.manager = true)
29
+ ${companyId ? 'AND ue.company_id = $2' : ''}
30
30
  LIMIT 1
31
31
  `, companyId ? [email, companyId] : [email]);
32
32
 
@@ -34,7 +34,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
34
34
  return createErrorResponse(403, 'Manager or Admin access required');
35
35
  }
36
36
 
37
- const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
37
+ const userCompanyId = companyId || accessCheck.rows[0].company_id;
38
38
  const periodDays = parsePeriod(period);
39
39
  const periodStart = new Date();
40
40
  periodStart.setDate(periodStart.getDate() - periodDays);
@@ -49,7 +49,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
49
49
  AVG(s.duration_seconds / 60.0) as avg_session_minutes
50
50
  FROM rapport.sessions s
51
51
  JOIN rapport.projects p ON s.project_id = p.project_id
52
- WHERE p."Company_ID" = $1
52
+ WHERE p.company_id = $1
53
53
  AND s.started_at >= $2
54
54
  `, [userCompanyId, periodStart]);
55
55
  } catch (e) {
@@ -69,7 +69,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
69
69
  AVG(dm.compliance_score) as avg_compliance
70
70
  FROM rapport.developer_metrics dm
71
71
  JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
72
- WHERE r."Company_ID" = $1
72
+ WHERE r.company_id = $1
73
73
  AND dm.period_start >= $2
74
74
  GROUP BY dm.developer_email, dm.developer_name
75
75
  ORDER BY total_commits DESC
@@ -86,7 +86,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
86
86
  SELECT DISTINCT s.email_address
87
87
  FROM rapport.sessions s
88
88
  JOIN rapport.projects p ON s.project_id = p.project_id
89
- WHERE p."Company_ID" = $1
89
+ WHERE p.company_id = $1
90
90
  AND s.started_at >= $2
91
91
  `, [userCompanyId, periodStart]);
92
92
  } catch (e) {
@@ -106,6 +106,9 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
106
106
  };
107
107
 
108
108
  const sessionData = sessionMetrics.rows[0] || {};
109
+ const multiplier = calculateMultiplier(aiAssistedDevs, nonAiDevs);
110
+ const totalCommits = devMetrics.rows.reduce((sum, d) => sum + parseInt(d.total_commits || 0), 0);
111
+ const aiCommits = aiAssistedDevs.reduce((sum, d) => sum + parseInt(d.total_commits || 0), 0);
109
112
 
110
113
  return createSuccessResponse({
111
114
  report_type: 'ai_leverage',
@@ -113,28 +116,29 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
113
116
  period_start: periodStart.toISOString(),
114
117
  period_end: new Date().toISOString(),
115
118
  summary: {
116
- ai_sessions: parseInt(sessionData.total_sessions) || 0,
117
- ai_users: aiAssistedDevs.length,
118
- non_ai_users: nonAiDevs.length,
119
- avg_session_minutes: parseFloat(sessionData.avg_session_minutes || 0).toFixed(1)
119
+ total_sessions: parseInt(sessionData.total_sessions) || 0,
120
+ unique_ai_users: aiAssistedDevs.length,
121
+ avg_session_minutes: parseFloat(sessionData.avg_session_minutes || 0).toFixed(1),
122
+ ai_assisted_commits: aiCommits,
123
+ total_commits: totalCommits,
124
+ ai_commit_percentage: totalCommits > 0 ? ((aiCommits / totalCommits) * 100).toFixed(1) : '0',
125
+ productivity_multiplier: multiplier.available ? multiplier.value : '1.0'
120
126
  },
121
- ai_assisted: {
122
- developers_count: aiAssistedDevs.length,
123
- avg_commits: calcAvg(aiAssistedDevs, 'total_commits').toFixed(1),
124
- avg_lines: calcAvg(aiAssistedDevs, 'lines_added').toFixed(0),
125
- avg_prs: calcAvg(aiAssistedDevs, 'prs_merged').toFixed(1),
126
- avg_compliance: calcAvg(aiAssistedDevs, 'avg_compliance').toFixed(1),
127
- developers: aiAssistedDevs
127
+ comparison: {
128
+ ai_assisted: {
129
+ developer_count: aiAssistedDevs.length,
130
+ avg_commits_per_dev: calcAvg(aiAssistedDevs, 'total_commits').toFixed(1),
131
+ avg_lines_per_dev: calcAvg(aiAssistedDevs, 'lines_added').toFixed(0),
132
+ avg_compliance: calcAvg(aiAssistedDevs, 'avg_compliance').toFixed(1)
133
+ },
134
+ non_ai: {
135
+ developer_count: nonAiDevs.length,
136
+ avg_commits_per_dev: calcAvg(nonAiDevs, 'total_commits').toFixed(1),
137
+ avg_lines_per_dev: calcAvg(nonAiDevs, 'lines_added').toFixed(0),
138
+ avg_compliance: calcAvg(nonAiDevs, 'avg_compliance').toFixed(1)
139
+ }
128
140
  },
129
- non_ai: {
130
- developers_count: nonAiDevs.length,
131
- avg_commits: calcAvg(nonAiDevs, 'total_commits').toFixed(1),
132
- avg_lines: calcAvg(nonAiDevs, 'lines_added').toFixed(0),
133
- avg_prs: calcAvg(nonAiDevs, 'prs_merged').toFixed(1),
134
- avg_compliance: calcAvg(nonAiDevs, 'avg_compliance').toFixed(1),
135
- developers: nonAiDevs
136
- },
137
- productivity_multiplier: calculateMultiplier(aiAssistedDevs, nonAiDevs),
141
+ standards_effectiveness: [],
138
142
  insights: generateInsights(aiAssistedDevs, nonAiDevs, sessionData)
139
143
  });
140
144
  });