@equilateral_ai/mindmeld 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. package/src/index.js +969 -0
@@ -0,0 +1,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
+ }