@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.
- package/README.md +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-end.js +112 -3
- package/hooks/session-start.js +635 -41
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/mindmeldMcpCore.js +594 -0
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +15 -11
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
|
@@ -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.
|
|
43
|
-
FROM
|
|
44
|
-
JOIN rapport.projects p ON p.
|
|
45
|
-
WHERE ue.
|
|
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.
|
|
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].
|
|
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
|
|
54
|
+
// Fetch developer metrics — join pattern_usage through sessions for project scope
|
|
61
55
|
const developerResult = await executeQuery(`
|
|
62
56
|
SELECT
|
|
63
|
-
u.
|
|
64
|
-
u.
|
|
65
|
-
COALESCE(
|
|
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
|
|
76
|
-
JOIN
|
|
77
|
-
LEFT JOIN
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
|
87
|
-
GROUP BY u.
|
|
88
|
-
|
|
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
|
-
//
|
|
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
|
|
140
|
-
const rawScore = (patternsUsed * 2) + (standardsAdherence * 50)
|
|
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:
|
|
159
|
-
violations_by_category:
|
|
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:
|
|
129
|
+
total_violations: 0,
|
|
200
130
|
period
|
|
201
131
|
},
|
|
202
132
|
developers,
|
|
203
|
-
trend_data:
|
|
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);
|