@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
@@ -0,0 +1,236 @@
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
+ }
@@ -7,11 +7,6 @@
7
7
  * - project_id (required) - Project to scope metrics
8
8
  * - period (optional, default: 30d) - 30d or 90d
9
9
  *
10
- * Returns:
11
- * - team_summary: aggregate quality metrics
12
- * - developers: per-developer scores, violations, trends
13
- * - trend_data: daily average scores for charting
14
- *
15
10
  * Auth: Cognito JWT required, Manager or Admin role
16
11
  */
17
12
 
@@ -32,19 +27,18 @@ async function getDeveloperScores({ requestContext, queryStringParameters }) {
32
27
  return createErrorResponse(400, 'project_id is required');
33
28
  }
34
29
 
35
- // Validate period parameter
36
30
  if (!['30d', '90d'].includes(period)) {
37
31
  return createErrorResponse(400, 'period must be 30d or 90d');
38
32
  }
39
33
 
40
34
  // Validate access - must be manager/admin with access to this project
41
35
  const accessCheck = await executeQuery(`
42
- SELECT ue."Company_ID"
43
- FROM "UserEntitlements" ue
44
- JOIN rapport.projects p ON p."Company_ID" = ue."Company_ID"
45
- WHERE ue."Email_Address" = $1
36
+ SELECT ue.company_id
37
+ FROM rapport.user_entitlements ue
38
+ JOIN rapport.projects p ON p.company_id = ue.company_id
39
+ WHERE ue.email_address = $1
46
40
  AND p.project_id = $2
47
- AND (ue."Admin" = true OR ue."Manager" = true)
41
+ AND (ue.admin = true OR ue.manager = true)
48
42
  LIMIT 1
49
43
  `, [email, projectId]);
50
44
 
@@ -52,112 +46,66 @@ async function getDeveloperScores({ requestContext, queryStringParameters }) {
52
46
  return createErrorResponse(403, 'Manager or Admin access required for this project');
53
47
  }
54
48
 
55
- const companyId = accessCheck.rows[0].Company_ID;
49
+ const companyId = accessCheck.rows[0].company_id;
56
50
  const periodDays = period === '90d' ? 90 : 30;
57
51
  const periodStart = new Date();
58
52
  periodStart.setDate(periodStart.getDate() - periodDays);
59
53
 
60
- // Fetch developer metrics for the project
54
+ // Fetch developer metrics — join pattern_usage through sessions for project scope
61
55
  const developerResult = await executeQuery(`
62
56
  SELECT
63
- u."Email_Address" as user_email,
64
- u."User_Display_Name" as display_name,
65
- COALESCE(SUM(pu.usage_count), 0) as patterns_used,
66
- COALESCE(
67
- (SELECT COUNT(*) FROM rapport.pattern_violations pv
68
- WHERE pv.developer_email = u."Email_Address"
69
- AND pv.project_id = $2
70
- AND pv.detected_at >= $3),
71
- 0
72
- ) as violations,
73
- COALESCE(dm.compliance_score, 0) as compliance_score,
57
+ u.email_address as user_email,
58
+ CONCAT(u.first_name, ' ', u.last_name) as display_name,
59
+ COALESCE(pu_counts.patterns_used, 0) as patterns_used,
60
+ COALESCE(dm_scores.compliance_score, 0) as compliance_score,
74
61
  MAX(sc.session_started_at) as last_active
75
- FROM "UserEntitlements" ue
76
- JOIN "Users" u ON u."Email_Address" = ue."Email_Address"
77
- LEFT JOIN rapport.pattern_usage pu ON pu.email_address = u."Email_Address"
78
- AND pu.project_id = $2
62
+ FROM rapport.user_entitlements ue
63
+ JOIN rapport.users u ON u.email_address = ue.email_address
64
+ LEFT JOIN (
65
+ SELECT pu.email_address, COUNT(*) as patterns_used
66
+ FROM rapport.pattern_usage pu
67
+ JOIN rapport.sessions s ON pu.session_id = s.session_id
68
+ WHERE s.project_id = $2
79
69
  AND pu.used_at >= $3
80
- LEFT JOIN rapport.developer_metrics dm ON dm.developer_email = u."Email_Address"
81
- AND dm.project_id = $2
70
+ GROUP BY pu.email_address
71
+ ) pu_counts ON pu_counts.email_address = u.email_address
72
+ LEFT JOIN (
73
+ SELECT dm.developer_email, AVG(dm.compliance_score) as compliance_score
74
+ FROM rapport.developer_metrics dm
75
+ JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
76
+ JOIN rapport.projects p ON p.repo_url = r.repo_url
77
+ WHERE p.project_id = $2
82
78
  AND dm.period_start >= $3
83
- LEFT JOIN rapport.session_correlations sc ON sc.email_address = u."Email_Address"
79
+ GROUP BY dm.developer_email
80
+ ) dm_scores ON dm_scores.developer_email = u.email_address
81
+ LEFT JOIN rapport.session_correlations sc ON sc.email_address = u.email_address
84
82
  AND sc.project_id = $2
85
83
  AND sc.session_started_at >= $3
86
- WHERE ue."Company_ID" = $1
87
- GROUP BY u."Email_Address", u."User_Display_Name", dm.compliance_score
88
- ORDER BY u."User_Display_Name" ASC
84
+ WHERE ue.company_id = $1
85
+ GROUP BY u.email_address, u.first_name, u.last_name,
86
+ pu_counts.patterns_used, dm_scores.compliance_score
87
+ ORDER BY u.first_name ASC, u.last_name ASC
89
88
  `, [companyId, projectId, periodStart]);
90
89
 
91
- // Fetch violation categories per developer
92
- const violationCategories = await executeQuery(`
93
- SELECT
94
- pv.developer_email,
95
- pv.category,
96
- COUNT(*) as count
97
- FROM rapport.pattern_violations pv
98
- WHERE pv.project_id = $1
99
- AND pv.detected_at >= $2
100
- GROUP BY pv.developer_email, pv.category
101
- `, [projectId, periodStart]);
102
-
103
- // Build category lookup
104
- const categoryLookup = {};
105
- for (const row of violationCategories.rows) {
106
- if (!categoryLookup[row.developer_email]) {
107
- categoryLookup[row.developer_email] = {};
108
- }
109
- categoryLookup[row.developer_email][row.category] = parseInt(row.count);
110
- }
111
-
112
- // Fetch trend data for the previous period to determine direction
113
- const previousPeriodStart = new Date();
114
- previousPeriodStart.setDate(previousPeriodStart.getDate() - (periodDays * 2));
115
-
116
- const previousScores = await executeQuery(`
117
- SELECT
118
- dm.developer_email,
119
- AVG(dm.compliance_score) as prev_avg_score
120
- FROM rapport.developer_metrics dm
121
- WHERE dm.project_id = $1
122
- AND dm.period_start >= $2
123
- AND dm.period_start < $3
124
- GROUP BY dm.developer_email
125
- `, [projectId, previousPeriodStart, periodStart]);
126
-
127
- const previousScoreLookup = {};
128
- for (const row of previousScores.rows) {
129
- previousScoreLookup[row.developer_email] = parseFloat(row.prev_avg_score);
130
- }
131
-
132
- // Calculate quality scores and build developer list
90
+ // Build developer list with quality scores
133
91
  const developers = developerResult.rows.map(row => {
134
92
  const patternsUsed = parseInt(row.patterns_used) || 0;
135
- const violations = parseInt(row.violations) || 0;
136
93
  const complianceScore = parseFloat(row.compliance_score) || 0;
137
94
  const standardsAdherence = complianceScore / 100;
138
95
 
139
- // Quality score formula: (patterns_used * 2 + standards_adherence * 50 - violations * 5), capped 0-100
140
- const rawScore = (patternsUsed * 2) + (standardsAdherence * 50) - (violations * 5);
96
+ // Quality score: patterns used + standards adherence, capped 0-100
97
+ const rawScore = (patternsUsed * 2) + (standardsAdherence * 50);
141
98
  const qualityScore = Math.max(0, Math.min(100, Math.round(rawScore)));
142
99
 
143
- // Determine trend
144
- const prevScore = previousScoreLookup[row.user_email];
145
- let trend = 'stable';
146
- if (prevScore !== undefined) {
147
- const scoreDiff = qualityScore - prevScore;
148
- if (scoreDiff > 5) trend = 'improving';
149
- else if (scoreDiff < -5) trend = 'declining';
150
- }
151
-
152
100
  return {
153
101
  user_email: row.user_email,
154
102
  display_name: row.display_name || row.user_email.split('@')[0],
155
103
  quality_score: qualityScore,
156
104
  standards_adherence: parseFloat(standardsAdherence.toFixed(2)),
157
105
  patterns_used: patternsUsed,
158
- violations: violations,
159
- violations_by_category: categoryLookup[row.user_email] || {},
160
- trend,
106
+ violations: 0,
107
+ violations_by_category: {},
108
+ trend: 'stable',
161
109
  last_active: row.last_active
162
110
  ? new Date(row.last_active).toISOString().split('T')[0]
163
111
  : null
@@ -172,35 +120,17 @@ async function getDeveloperScores({ requestContext, queryStringParameters }) {
172
120
  const avgAdherence = totalDevelopers > 0
173
121
  ? parseFloat((developers.reduce((sum, d) => sum + d.standards_adherence, 0) / totalDevelopers).toFixed(2))
174
122
  : 0;
175
- const totalViolations = developers.reduce((sum, d) => sum + d.violations, 0);
176
-
177
- // Fetch trend data (daily averages)
178
- const trendResult = await executeQuery(`
179
- SELECT
180
- DATE(dm.period_start) as date,
181
- AVG(dm.compliance_score) as avg_score
182
- FROM rapport.developer_metrics dm
183
- WHERE dm.project_id = $1
184
- AND dm.period_start >= $2
185
- GROUP BY DATE(dm.period_start)
186
- ORDER BY date ASC
187
- `, [projectId, periodStart]);
188
-
189
- const trendData = trendResult.rows.map(row => ({
190
- date: new Date(row.date).toISOString().split('T')[0],
191
- avg_score: Math.round(parseFloat(row.avg_score))
192
- }));
193
123
 
194
124
  return createSuccessResponse({
195
125
  team_summary: {
196
126
  total_developers: totalDevelopers,
197
127
  avg_quality_score: avgQualityScore,
198
128
  standards_adherence: avgAdherence,
199
- total_violations: totalViolations,
129
+ total_violations: 0,
200
130
  period
201
131
  },
202
132
  developers,
203
- trend_data: trendData
133
+ trend_data: []
204
134
  }, 'Developer scores retrieved successfully');
205
135
  }
206
136
 
@@ -82,7 +82,7 @@ async function inviteCollaborator({ body: requestBody = {}, requestContext }) {
82
82
  `, [inviteToken, projectId, targetEmail]);
83
83
 
84
84
  // Build invite URL
85
- const appUrl = process.env.APP_URL || 'https://mindmeld.dev';
85
+ const appUrl = process.env.APP_URL || 'https://app.mindmeld.dev';
86
86
  const inviteUrl = `${appUrl}/invite/accept?token=${inviteToken}`;
87
87
 
88
88
  // Send email
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Company Users Remove Handler
3
+ * Removes a user's entitlement from a company
4
+ * If the user was admin-paid, decrements license count and updates Stripe subscription quantity
5
+ *
6
+ * DELETE /api/company/users?company_id=xxx&email=xxx
7
+ * Auth: Cognito JWT required
8
+ */
9
+
10
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
11
+ const Stripe = require('stripe');
12
+
13
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
14
+
15
+ /**
16
+ * Remove user from company
17
+ * Requires admin access to the company
18
+ */
19
+ async function removeCompanyUser({ queryStringParameters: queryParams = {}, requestContext }) {
20
+ try {
21
+ const Request_ID = requestContext.requestId;
22
+ const callerEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
23
+ const companyId = queryParams.company_id;
24
+ const targetEmail = queryParams.email;
25
+
26
+ if (!callerEmail) {
27
+ return createErrorResponse(401, 'Authentication required');
28
+ }
29
+
30
+ if (!companyId || !targetEmail) {
31
+ return createErrorResponse(400, 'company_id and email query parameters are required');
32
+ }
33
+
34
+ // Prevent self-removal
35
+ if (targetEmail === callerEmail) {
36
+ return createErrorResponse(400, 'Cannot remove yourself from the company');
37
+ }
38
+
39
+ // Verify caller is admin of this company
40
+ const adminQuery = `
41
+ SELECT admin, client_id FROM rapport.user_entitlements
42
+ WHERE email_address = $1 AND company_id = $2
43
+ `;
44
+ const adminCheck = await executeQuery(adminQuery, [callerEmail, companyId]);
45
+
46
+ if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
47
+ return createErrorResponse(403, 'Admin access required to remove users');
48
+ }
49
+
50
+ const clientId = adminCheck.rows[0].client_id;
51
+
52
+ // Check the target user's billing_type before deleting
53
+ const targetQuery = `
54
+ SELECT billing_type FROM rapport.user_entitlements
55
+ WHERE email_address = $1 AND company_id = $2
56
+ `;
57
+ const targetResult = await executeQuery(targetQuery, [targetEmail, companyId]);
58
+
59
+ if (targetResult.rowCount === 0) {
60
+ return createErrorResponse(404, `User ${targetEmail} not found in this company`);
61
+ }
62
+
63
+ const billingType = targetResult.rows[0].billing_type;
64
+
65
+ // If admin-paid, decrement license count and update Stripe
66
+ let stripeUpdated = false;
67
+ if (billingType === 'admin_paid') {
68
+ // Get client's Stripe subscription
69
+ const clientQuery = `
70
+ SELECT stripe_subscription_id, license_count
71
+ FROM rapport.clients WHERE client_id = $1
72
+ `;
73
+ const clientResult = await executeQuery(clientQuery, [clientId]);
74
+ const clientRecord = clientResult.rows[0];
75
+
76
+ if (clientRecord?.stripe_subscription_id) {
77
+ // Decrement license_count (minimum 1 — the admin's own license)
78
+ const updateResult = await executeQuery(`
79
+ UPDATE rapport.clients
80
+ SET license_count = GREATEST(COALESCE(license_count, 1) - 1, 1), last_updated = CURRENT_TIMESTAMP
81
+ WHERE client_id = $1
82
+ RETURNING license_count
83
+ `, [clientId]);
84
+ const newLicenseCount = updateResult.rows[0].license_count;
85
+
86
+ // Update Stripe subscription quantity
87
+ try {
88
+ const subscription = await stripe.subscriptions.retrieve(clientRecord.stripe_subscription_id);
89
+ const item = subscription.items.data[0];
90
+ if (item) {
91
+ await stripe.subscriptions.update(clientRecord.stripe_subscription_id, {
92
+ items: [{ id: item.id, quantity: newLicenseCount }],
93
+ proration_behavior: 'none'
94
+ });
95
+ stripeUpdated = true;
96
+ console.log(`[License] Decreased Stripe quantity to ${newLicenseCount} for ${clientId}`);
97
+ }
98
+ } catch (stripeError) {
99
+ console.error('[License] Stripe update failed on removal:', stripeError.message);
100
+ // Revert license_count since Stripe failed
101
+ await executeQuery(`
102
+ UPDATE rapport.clients
103
+ SET license_count = COALESCE(license_count, 0) + 1, last_updated = CURRENT_TIMESTAMP
104
+ WHERE client_id = $1
105
+ `, [clientId]);
106
+ return createErrorResponse(500, 'Failed to update subscription. Please try again or contact support.');
107
+ }
108
+ }
109
+ }
110
+
111
+ // Delete the entitlement
112
+ const deleteQuery = `
113
+ DELETE FROM rapport.user_entitlements
114
+ WHERE email_address = $1 AND company_id = $2
115
+ RETURNING email_address
116
+ `;
117
+ const result = await executeQuery(deleteQuery, [targetEmail, companyId]);
118
+
119
+ const message = billingType === 'admin_paid'
120
+ ? `User ${targetEmail} removed and license released${stripeUpdated ? '' : ' (Stripe update pending)'}`
121
+ : `User ${targetEmail} removed from company`;
122
+
123
+ return createSuccessResponse(
124
+ { Records: result.rows },
125
+ message,
126
+ {
127
+ Total_Records: result.rowCount,
128
+ Request_ID,
129
+ Timestamp: new Date().toISOString(),
130
+ license_released: billingType === 'admin_paid',
131
+ stripe_updated: stripeUpdated
132
+ }
133
+ );
134
+
135
+ } catch (error) {
136
+ console.error('Handler Error:', error);
137
+ return handleError(error);
138
+ }
139
+ }
140
+
141
+ exports.handler = wrapHandler(removeCompanyUser);