@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.
- package/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Update Handler
|
|
3
|
+
* Updates project metadata
|
|
4
|
+
*
|
|
5
|
+
* PUT /api/projects/{projectId}
|
|
6
|
+
* Body: { project_name, description, private }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Update project
|
|
13
|
+
* Requires owner or admin role on project
|
|
14
|
+
*/
|
|
15
|
+
async function updateProject({ pathParameters = {}, body: requestBody = {}, requestContext }) {
|
|
16
|
+
try {
|
|
17
|
+
const Request_ID = requestContext.requestId;
|
|
18
|
+
// REST API: requestContext.authorizer.claims.email
|
|
19
|
+
// HTTP API: requestContext.authorizer.jwt.claims.email
|
|
20
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
21
|
+
const { projectId } = pathParameters;
|
|
22
|
+
const { project_name, description, private: isPrivate, repo_url } = requestBody;
|
|
23
|
+
|
|
24
|
+
if (!projectId) {
|
|
25
|
+
return createErrorResponse(400, 'projectId is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check user has access to project
|
|
29
|
+
const accessQuery = `
|
|
30
|
+
SELECT
|
|
31
|
+
p.project_id,
|
|
32
|
+
p.company_id,
|
|
33
|
+
pc.role,
|
|
34
|
+
ue."Admin" as company_admin
|
|
35
|
+
FROM rapport.projects p
|
|
36
|
+
LEFT JOIN rapport.project_collaborators pc
|
|
37
|
+
ON p.project_id = pc.project_id
|
|
38
|
+
AND pc.email_address = $1
|
|
39
|
+
LEFT JOIN "UserEntitlements" ue
|
|
40
|
+
ON ue."Email_Address" = $1
|
|
41
|
+
AND ue."Company_ID" = p.company_id
|
|
42
|
+
WHERE p.project_id = $2
|
|
43
|
+
`;
|
|
44
|
+
const accessCheck = await executeQuery(accessQuery, [email, projectId]);
|
|
45
|
+
|
|
46
|
+
if (accessCheck.rowCount === 0) {
|
|
47
|
+
return createErrorResponse(404, 'Project not found');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const access = accessCheck.rows[0];
|
|
51
|
+
|
|
52
|
+
// Check permissions (owner, admin collaborator, or company admin)
|
|
53
|
+
const hasAccess = access.role === 'owner' || access.company_admin === true;
|
|
54
|
+
if (!hasAccess) {
|
|
55
|
+
return createErrorResponse(403, 'Insufficient permissions to update project');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build update query dynamically
|
|
59
|
+
const updates = [];
|
|
60
|
+
const values = [];
|
|
61
|
+
let paramIndex = 1;
|
|
62
|
+
|
|
63
|
+
if (project_name !== undefined) {
|
|
64
|
+
updates.push(`project_name = $${paramIndex++}`);
|
|
65
|
+
values.push(project_name);
|
|
66
|
+
}
|
|
67
|
+
if (description !== undefined) {
|
|
68
|
+
updates.push(`description = $${paramIndex++}`);
|
|
69
|
+
values.push(description);
|
|
70
|
+
}
|
|
71
|
+
if (isPrivate !== undefined) {
|
|
72
|
+
updates.push(`private = $${paramIndex++}`);
|
|
73
|
+
values.push(isPrivate);
|
|
74
|
+
}
|
|
75
|
+
if (repo_url !== undefined) {
|
|
76
|
+
updates.push(`repo_url = $${paramIndex++}`);
|
|
77
|
+
values.push(repo_url);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (updates.length === 0) {
|
|
81
|
+
return createErrorResponse(400, 'No fields to update');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
values.push(projectId);
|
|
85
|
+
|
|
86
|
+
const query = `
|
|
87
|
+
UPDATE rapport.projects
|
|
88
|
+
SET ${updates.join(', ')}, updated_at = NOW()
|
|
89
|
+
WHERE project_id = $${paramIndex}
|
|
90
|
+
RETURNING
|
|
91
|
+
project_id,
|
|
92
|
+
company_id,
|
|
93
|
+
project_name,
|
|
94
|
+
description,
|
|
95
|
+
private,
|
|
96
|
+
repo_url,
|
|
97
|
+
updated_at
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const result = await executeQuery(query, values);
|
|
101
|
+
|
|
102
|
+
return createSuccessResponse(
|
|
103
|
+
{ Records: result.rows },
|
|
104
|
+
'Project updated successfully',
|
|
105
|
+
{
|
|
106
|
+
Total_Records: result.rowCount,
|
|
107
|
+
Request_ID,
|
|
108
|
+
Timestamp: new Date().toISOString()
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Handler Error:', error);
|
|
114
|
+
return handleError(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
exports.handler = wrapHandler(updateProject);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Leverage Report Handler
|
|
3
|
+
* Compares AI-assisted vs non-AI development metrics
|
|
4
|
+
*
|
|
5
|
+
* GET /api/reports/ai-leverage
|
|
6
|
+
* Query: ?period=7d|30d|90d&Company_ID=xxx
|
|
7
|
+
* Auth: Cognito JWT required, Manager or Admin role
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Unauthorized');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const params = queryStringParameters || {};
|
|
20
|
+
const period = params.period || '30d';
|
|
21
|
+
const companyId = params.Company_ID;
|
|
22
|
+
|
|
23
|
+
// Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
|
|
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' : ''}
|
|
30
|
+
LIMIT 1
|
|
31
|
+
`, companyId ? [email, companyId] : [email]);
|
|
32
|
+
|
|
33
|
+
if (accessCheck.rows.length === 0) {
|
|
34
|
+
return createErrorResponse(403, 'Manager or Admin access required');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
|
|
38
|
+
const periodDays = parsePeriod(period);
|
|
39
|
+
const periodStart = new Date();
|
|
40
|
+
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
41
|
+
|
|
42
|
+
// Session metrics (AI-assisted) - from Claude Code sessions
|
|
43
|
+
let sessionMetrics = { rows: [{ total_sessions: 0, unique_users: 0, avg_session_minutes: 0 }] };
|
|
44
|
+
try {
|
|
45
|
+
sessionMetrics = await executeQuery(`
|
|
46
|
+
SELECT
|
|
47
|
+
COUNT(DISTINCT s.session_id) as total_sessions,
|
|
48
|
+
COUNT(DISTINCT s.email_address) as unique_users,
|
|
49
|
+
AVG(s.duration_seconds / 60.0) as avg_session_minutes
|
|
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
|
+
`, [userCompanyId, periodStart]);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Table might not have data
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Developer productivity metrics
|
|
60
|
+
let devMetrics = { rows: [] };
|
|
61
|
+
try {
|
|
62
|
+
devMetrics = await executeQuery(`
|
|
63
|
+
SELECT
|
|
64
|
+
dm.developer_email,
|
|
65
|
+
dm.developer_name,
|
|
66
|
+
SUM(dm.commit_count) as total_commits,
|
|
67
|
+
SUM(dm.lines_added) as lines_added,
|
|
68
|
+
SUM(dm.prs_merged) as prs_merged,
|
|
69
|
+
AVG(dm.compliance_score) as avg_compliance
|
|
70
|
+
FROM rapport.developer_metrics dm
|
|
71
|
+
JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
|
|
72
|
+
WHERE r."Company_ID" = $1
|
|
73
|
+
AND dm.period_start >= $2
|
|
74
|
+
GROUP BY dm.developer_email, dm.developer_name
|
|
75
|
+
ORDER BY total_commits DESC
|
|
76
|
+
LIMIT 20
|
|
77
|
+
`, [userCompanyId, periodStart]);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// No metrics yet
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Users with Claude Code sessions
|
|
83
|
+
let aiUsers = { rows: [] };
|
|
84
|
+
try {
|
|
85
|
+
aiUsers = await executeQuery(`
|
|
86
|
+
SELECT DISTINCT s.email_address
|
|
87
|
+
FROM rapport.sessions s
|
|
88
|
+
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
89
|
+
WHERE p."Company_ID" = $1
|
|
90
|
+
AND s.started_at >= $2
|
|
91
|
+
`, [userCompanyId, periodStart]);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// No sessions
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const aiUserEmails = new Set(aiUsers.rows.map(r => r.email_address));
|
|
97
|
+
|
|
98
|
+
// Classify developers as AI-assisted or not
|
|
99
|
+
const aiAssistedDevs = devMetrics.rows.filter(d => aiUserEmails.has(d.developer_email));
|
|
100
|
+
const nonAiDevs = devMetrics.rows.filter(d => !aiUserEmails.has(d.developer_email));
|
|
101
|
+
|
|
102
|
+
// Calculate averages
|
|
103
|
+
const calcAvg = (devs, field) => {
|
|
104
|
+
if (devs.length === 0) return 0;
|
|
105
|
+
return devs.reduce((sum, d) => sum + parseFloat(d[field] || 0), 0) / devs.length;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const sessionData = sessionMetrics.rows[0] || {};
|
|
109
|
+
|
|
110
|
+
return createSuccessResponse({
|
|
111
|
+
report_type: 'ai_leverage',
|
|
112
|
+
period: period,
|
|
113
|
+
period_start: periodStart.toISOString(),
|
|
114
|
+
period_end: new Date().toISOString(),
|
|
115
|
+
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)
|
|
120
|
+
},
|
|
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
|
|
128
|
+
},
|
|
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),
|
|
138
|
+
insights: generateInsights(aiAssistedDevs, nonAiDevs, sessionData)
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
function calculateMultiplier(aiDevs, nonAiDevs) {
|
|
143
|
+
if (aiDevs.length === 0 || nonAiDevs.length === 0) {
|
|
144
|
+
return { available: false, message: 'Need both AI and non-AI developers to calculate' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const aiAvgCommits = aiDevs.reduce((s, d) => s + parseFloat(d.total_commits || 0), 0) / aiDevs.length;
|
|
148
|
+
const nonAiAvgCommits = nonAiDevs.reduce((s, d) => s + parseFloat(d.total_commits || 0), 0) / nonAiDevs.length;
|
|
149
|
+
|
|
150
|
+
if (nonAiAvgCommits === 0) {
|
|
151
|
+
return { available: false, message: 'Non-AI baseline has no commits' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
available: true,
|
|
156
|
+
value: (aiAvgCommits / nonAiAvgCommits).toFixed(2),
|
|
157
|
+
interpretation: aiAvgCommits > nonAiAvgCommits
|
|
158
|
+
? 'AI-assisted developers are more productive'
|
|
159
|
+
: 'Non-AI developers are more productive'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function generateInsights(aiDevs, nonAiDevs, sessionData) {
|
|
164
|
+
const insights = [];
|
|
165
|
+
|
|
166
|
+
const totalSessions = parseInt(sessionData.total_sessions) || 0;
|
|
167
|
+
|
|
168
|
+
if (totalSessions === 0) {
|
|
169
|
+
insights.push({
|
|
170
|
+
type: 'info',
|
|
171
|
+
message: 'No Claude Code sessions detected. Install Claude Code CLI to start tracking AI-assisted development.'
|
|
172
|
+
});
|
|
173
|
+
return insights;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (aiDevs.length > 0) {
|
|
177
|
+
const aiAvgCompliance = aiDevs.reduce((s, d) => s + parseFloat(d.avg_compliance || 0), 0) / aiDevs.length;
|
|
178
|
+
const nonAiAvgCompliance = nonAiDevs.length > 0
|
|
179
|
+
? nonAiDevs.reduce((s, d) => s + parseFloat(d.avg_compliance || 0), 0) / nonAiDevs.length
|
|
180
|
+
: 0;
|
|
181
|
+
|
|
182
|
+
if (aiAvgCompliance > nonAiAvgCompliance + 10) {
|
|
183
|
+
insights.push({
|
|
184
|
+
type: 'positive',
|
|
185
|
+
message: `AI-assisted developers have ${(aiAvgCompliance - nonAiAvgCompliance).toFixed(0)}% higher compliance scores`
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return insights;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parsePeriod(period) {
|
|
194
|
+
const match = period.match(/^(\d+)([dwm])$/);
|
|
195
|
+
if (!match) return 30;
|
|
196
|
+
|
|
197
|
+
const [, num, unit] = match;
|
|
198
|
+
const n = parseInt(num);
|
|
199
|
+
|
|
200
|
+
switch (unit) {
|
|
201
|
+
case 'd': return n;
|
|
202
|
+
case 'w': return n * 7;
|
|
203
|
+
case 'm': return n * 30;
|
|
204
|
+
default: return 30;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engineering Investment Report Handler
|
|
3
|
+
* Jellyfish-style "where is engineering time going" report
|
|
4
|
+
*
|
|
5
|
+
* GET /api/reports/engineering-investment
|
|
6
|
+
* Query: ?period=7d|30d|90d&Company_ID=xxx
|
|
7
|
+
* Auth: Cognito JWT required, Manager or Admin role
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Unauthorized');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const params = queryStringParameters || {};
|
|
20
|
+
const period = params.period || '7d';
|
|
21
|
+
const companyId = params.Company_ID;
|
|
22
|
+
|
|
23
|
+
// Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
|
|
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' : ''}
|
|
30
|
+
LIMIT 1
|
|
31
|
+
`, companyId ? [email, companyId] : [email]);
|
|
32
|
+
|
|
33
|
+
if (accessCheck.rows.length === 0) {
|
|
34
|
+
return createErrorResponse(403, 'Manager or Admin access required');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
|
|
38
|
+
const periodDays = parsePeriod(period);
|
|
39
|
+
const periodStart = new Date();
|
|
40
|
+
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
41
|
+
|
|
42
|
+
// Get engineering investment breakdown
|
|
43
|
+
const investment = await executeQuery(`
|
|
44
|
+
WITH commit_modules AS (
|
|
45
|
+
SELECT
|
|
46
|
+
COALESCE(SUBSTRING(c.file_path FROM '^([^/]+)'), 'root') as module,
|
|
47
|
+
COUNT(*) as commit_count,
|
|
48
|
+
SUM(c.lines_added) as lines_added,
|
|
49
|
+
SUM(c.lines_removed) as lines_removed,
|
|
50
|
+
COUNT(DISTINCT c.author_email) as contributors
|
|
51
|
+
FROM rapport.commits c
|
|
52
|
+
JOIN rapport.git_repositories r ON c.repo_id = r.repo_id
|
|
53
|
+
WHERE r.Company_ID = $1
|
|
54
|
+
AND c.committed_at >= $2
|
|
55
|
+
GROUP BY module
|
|
56
|
+
)
|
|
57
|
+
SELECT
|
|
58
|
+
module,
|
|
59
|
+
commit_count,
|
|
60
|
+
lines_added,
|
|
61
|
+
lines_removed,
|
|
62
|
+
contributors,
|
|
63
|
+
ROUND(commit_count * 100.0 / SUM(commit_count) OVER (), 1) as commit_percentage,
|
|
64
|
+
ROUND((lines_added + lines_removed) * 100.0 / SUM(lines_added + lines_removed) OVER (), 1) as churn_percentage
|
|
65
|
+
FROM commit_modules
|
|
66
|
+
ORDER BY commit_count DESC
|
|
67
|
+
LIMIT 20
|
|
68
|
+
`, [userCompanyId, periodStart]);
|
|
69
|
+
|
|
70
|
+
// Get top contributors
|
|
71
|
+
const contributors = await executeQuery(`
|
|
72
|
+
SELECT
|
|
73
|
+
dm.developer_name,
|
|
74
|
+
dm.developer_email,
|
|
75
|
+
SUM(dm.commit_count) as total_commits,
|
|
76
|
+
SUM(dm.lines_added) as total_lines_added,
|
|
77
|
+
SUM(dm.prs_merged) as total_prs_merged,
|
|
78
|
+
AVG(dm.compliance_score) as avg_compliance
|
|
79
|
+
FROM rapport.developer_metrics dm
|
|
80
|
+
JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
|
|
81
|
+
WHERE r.Company_ID = $1
|
|
82
|
+
AND dm.period_start >= $2
|
|
83
|
+
GROUP BY dm.developer_name, dm.developer_email
|
|
84
|
+
ORDER BY total_commits DESC
|
|
85
|
+
LIMIT 10
|
|
86
|
+
`, [userCompanyId, periodStart]);
|
|
87
|
+
|
|
88
|
+
// Get repo breakdown
|
|
89
|
+
const repos = await executeQuery(`
|
|
90
|
+
SELECT
|
|
91
|
+
r.repo_name,
|
|
92
|
+
COUNT(DISTINCT c.commit_sha) as commit_count,
|
|
93
|
+
COUNT(DISTINCT c.author_email) as contributors,
|
|
94
|
+
SUM(c.lines_added) as lines_added
|
|
95
|
+
FROM rapport.git_repositories r
|
|
96
|
+
LEFT JOIN rapport.commits c ON r.repo_id = c.repo_id AND c.committed_at >= $2
|
|
97
|
+
WHERE r.Company_ID = $1
|
|
98
|
+
GROUP BY r.repo_id, r.repo_name
|
|
99
|
+
ORDER BY commit_count DESC
|
|
100
|
+
`, [userCompanyId, periodStart]);
|
|
101
|
+
|
|
102
|
+
return createSuccessResponse({
|
|
103
|
+
report_type: 'engineering_investment',
|
|
104
|
+
period: period,
|
|
105
|
+
period_start: periodStart.toISOString(),
|
|
106
|
+
period_end: new Date().toISOString(),
|
|
107
|
+
summary: {
|
|
108
|
+
total_commits: investment.rows.reduce((sum, r) => sum + parseInt(r.commit_count), 0),
|
|
109
|
+
total_contributors: new Set(contributors.rows.map(r => r.developer_email)).size,
|
|
110
|
+
total_repos: repos.rows.length,
|
|
111
|
+
total_lines_changed: investment.rows.reduce((sum, r) => sum + parseInt(r.lines_added || 0) + parseInt(r.lines_removed || 0), 0)
|
|
112
|
+
},
|
|
113
|
+
investment_by_module: investment.rows,
|
|
114
|
+
top_contributors: contributors.rows,
|
|
115
|
+
repository_breakdown: repos.rows
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
function parsePeriod(period) {
|
|
120
|
+
const match = period.match(/^(\d+)([dwm])$/);
|
|
121
|
+
if (!match) return 7;
|
|
122
|
+
|
|
123
|
+
const [, num, unit] = match;
|
|
124
|
+
const n = parseInt(num);
|
|
125
|
+
|
|
126
|
+
switch (unit) {
|
|
127
|
+
case 'd': return n;
|
|
128
|
+
case 'w': return n * 7;
|
|
129
|
+
case 'm': return n * 30;
|
|
130
|
+
default: return 7;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk Forecast Report Handler
|
|
3
|
+
* Identifies delivery risks, bus factors, and attention areas
|
|
4
|
+
*
|
|
5
|
+
* GET /api/reports/risk-forecast
|
|
6
|
+
* Query: ?Company_ID=xxx
|
|
7
|
+
* Auth: Cognito JWT required, Manager or Admin role
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Unauthorized');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const params = queryStringParameters || {};
|
|
20
|
+
const companyId = params.Company_ID;
|
|
21
|
+
|
|
22
|
+
// Validate access - must be manager/admin (boolean columns in Tim-Combo schema)
|
|
23
|
+
const accessCheck = await executeQuery(`
|
|
24
|
+
SELECT ue."Company_ID"
|
|
25
|
+
FROM "UserEntitlements" ue
|
|
26
|
+
WHERE ue."Email_Address" = $1
|
|
27
|
+
AND (ue."Admin" = true OR ue."Manager" = true)
|
|
28
|
+
${companyId ? 'AND ue."Company_ID" = $2' : ''}
|
|
29
|
+
LIMIT 1
|
|
30
|
+
`, companyId ? [email, companyId] : [email]);
|
|
31
|
+
|
|
32
|
+
if (accessCheck.rows.length === 0) {
|
|
33
|
+
return createErrorResponse(403, 'Manager or Admin access required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const userCompanyId = companyId || accessCheck.rows[0].Company_ID;
|
|
37
|
+
|
|
38
|
+
// Knowledge silos (bus factor analysis)
|
|
39
|
+
let silos = { rows: [] };
|
|
40
|
+
try {
|
|
41
|
+
silos = await executeQuery(`
|
|
42
|
+
SELECT
|
|
43
|
+
ks.module_path,
|
|
44
|
+
ks.primary_contributor,
|
|
45
|
+
ks.contribution_percentage,
|
|
46
|
+
ks.total_contributors,
|
|
47
|
+
ks.risk_level
|
|
48
|
+
FROM rapport.knowledge_silos ks
|
|
49
|
+
JOIN rapport.git_repositories r ON ks.repo_id = r.repo_id
|
|
50
|
+
WHERE r."Company_ID" = $1
|
|
51
|
+
ORDER BY
|
|
52
|
+
CASE ks.risk_level
|
|
53
|
+
WHEN 'critical' THEN 1
|
|
54
|
+
WHEN 'high' THEN 2
|
|
55
|
+
WHEN 'medium' THEN 3
|
|
56
|
+
ELSE 4
|
|
57
|
+
END
|
|
58
|
+
LIMIT 20
|
|
59
|
+
`, [userCompanyId]);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Table might not have data
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Stale PRs (open for more than 7 days)
|
|
65
|
+
let stalePRs = { rows: [] };
|
|
66
|
+
try {
|
|
67
|
+
stalePRs = await executeQuery(`
|
|
68
|
+
SELECT
|
|
69
|
+
pr.title,
|
|
70
|
+
pr.author_email,
|
|
71
|
+
pr.created_at,
|
|
72
|
+
EXTRACT(DAY FROM NOW() - pr.created_at) as days_open,
|
|
73
|
+
pr.files_changed,
|
|
74
|
+
pr.lines_added + pr.lines_removed as total_changes
|
|
75
|
+
FROM rapport.pull_requests pr
|
|
76
|
+
JOIN rapport.git_repositories r ON pr.repo_id = r.repo_id
|
|
77
|
+
WHERE r."Company_ID" = $1
|
|
78
|
+
AND pr.status = 'open'
|
|
79
|
+
AND pr.created_at < NOW() - INTERVAL '7 days'
|
|
80
|
+
ORDER BY pr.created_at ASC
|
|
81
|
+
LIMIT 10
|
|
82
|
+
`, [userCompanyId]);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// No PRs
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Working patterns - burnout risk (after-hours commits)
|
|
88
|
+
let burnoutRisk = { rows: [] };
|
|
89
|
+
try {
|
|
90
|
+
burnoutRisk = await executeQuery(`
|
|
91
|
+
SELECT
|
|
92
|
+
wp.developer_email,
|
|
93
|
+
SUM(wp.weekend_commits) as weekend_commits,
|
|
94
|
+
SUM(wp.after_hours_commits) as after_hours_commits,
|
|
95
|
+
wp.peak_hour
|
|
96
|
+
FROM rapport.working_patterns wp
|
|
97
|
+
JOIN rapport.git_repositories r ON wp.repo_id = r.repo_id
|
|
98
|
+
WHERE r."Company_ID" = $1
|
|
99
|
+
AND wp.period_start >= NOW() - INTERVAL '30 days'
|
|
100
|
+
GROUP BY wp.developer_email, wp.peak_hour
|
|
101
|
+
HAVING SUM(wp.after_hours_commits) > 10 OR SUM(wp.weekend_commits) > 5
|
|
102
|
+
ORDER BY (SUM(wp.after_hours_commits) + SUM(wp.weekend_commits)) DESC
|
|
103
|
+
LIMIT 10
|
|
104
|
+
`, [userCompanyId]);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
// No working patterns data
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Calculate overall risk score
|
|
110
|
+
const criticalSilos = silos.rows.filter(s => s.risk_level === 'critical').length;
|
|
111
|
+
const highSilos = silos.rows.filter(s => s.risk_level === 'high').length;
|
|
112
|
+
const staleCount = stalePRs.rows.length;
|
|
113
|
+
const burnoutCount = burnoutRisk.rows.length;
|
|
114
|
+
|
|
115
|
+
const riskScore = Math.min(100, criticalSilos * 20 + highSilos * 10 + staleCount * 5 + burnoutCount * 5);
|
|
116
|
+
|
|
117
|
+
return createSuccessResponse({
|
|
118
|
+
report_type: 'risk_forecast',
|
|
119
|
+
generated_at: new Date().toISOString(),
|
|
120
|
+
risk_score: {
|
|
121
|
+
value: riskScore,
|
|
122
|
+
level: riskScore >= 70 ? 'critical' : riskScore >= 40 ? 'elevated' : 'normal',
|
|
123
|
+
factors: {
|
|
124
|
+
knowledge_silos: criticalSilos + highSilos,
|
|
125
|
+
stale_prs: staleCount,
|
|
126
|
+
burnout_indicators: burnoutCount
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
knowledge_silos: silos.rows,
|
|
130
|
+
stale_pull_requests: stalePRs.rows,
|
|
131
|
+
burnout_indicators: burnoutRisk.rows,
|
|
132
|
+
recommendations: generateRecommendations(silos.rows, stalePRs.rows, burnoutRisk.rows)
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function generateRecommendations(silos, stalePRs, burnout) {
|
|
137
|
+
const recommendations = [];
|
|
138
|
+
|
|
139
|
+
// Knowledge silo recommendations
|
|
140
|
+
const criticalSilos = silos.filter(s => s.risk_level === 'critical');
|
|
141
|
+
if (criticalSilos.length > 0) {
|
|
142
|
+
recommendations.push({
|
|
143
|
+
priority: 'high',
|
|
144
|
+
category: 'Knowledge Sharing',
|
|
145
|
+
message: `${criticalSilos.length} critical knowledge silo(s) detected. Only one developer knows these areas.`,
|
|
146
|
+
action: 'Schedule pair programming or code review sessions to spread knowledge.',
|
|
147
|
+
modules: criticalSilos.map(s => s.module_path)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Stale PR recommendations
|
|
152
|
+
if (stalePRs.length > 0) {
|
|
153
|
+
const veryStale = stalePRs.filter(pr => parseInt(pr.days_open) > 14);
|
|
154
|
+
if (veryStale.length > 0) {
|
|
155
|
+
recommendations.push({
|
|
156
|
+
priority: 'medium',
|
|
157
|
+
category: 'Code Review',
|
|
158
|
+
message: `${veryStale.length} PR(s) open for more than 2 weeks.`,
|
|
159
|
+
action: 'Review and either merge, close, or request updates on these PRs.',
|
|
160
|
+
prs: veryStale.map(pr => pr.title)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Burnout recommendations
|
|
166
|
+
if (burnout.length > 0) {
|
|
167
|
+
recommendations.push({
|
|
168
|
+
priority: 'high',
|
|
169
|
+
category: 'Team Health',
|
|
170
|
+
message: `${burnout.length} developer(s) showing potential burnout indicators (frequent after-hours/weekend commits).`,
|
|
171
|
+
action: 'Check in with these team members about workload and work-life balance.',
|
|
172
|
+
developers: burnout.map(b => b.developer_email)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (recommendations.length === 0) {
|
|
177
|
+
recommendations.push({
|
|
178
|
+
priority: 'info',
|
|
179
|
+
category: 'Status',
|
|
180
|
+
message: 'No significant risks detected.',
|
|
181
|
+
action: 'Continue monitoring and maintaining healthy development practices.'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return recommendations;
|
|
186
|
+
}
|