@equilateral_ai/mindmeld 3.5.3 → 4.0.1

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 (139) hide show
  1. package/hooks/session-start.js +312 -85
  2. package/package.json +20 -14
  3. package/scripts/init-project.js +9 -23
  4. package/src/client/dbShim.js +16 -0
  5. package/src/core/AuthManager.js +3 -2
  6. package/src/handlers/helpers/dbOperations.js +9 -46
  7. package/src/index.js +2 -217
  8. package/src/utils/piiMask.js +16 -0
  9. package/scripts/harvest.js +0 -601
  10. package/scripts/inject.js +0 -409
  11. package/scripts/mcp-bridge.js +0 -220
  12. package/scripts/repo-analyzer.js +0 -870
  13. package/scripts/standards.js +0 -285
  14. package/src/collaboration/CollaborationPrompt.js +0 -460
  15. package/src/core/AlertEngine.js +0 -813
  16. package/src/core/AlertNotifier.js +0 -363
  17. package/src/core/CorrelationAnalyzer.js +0 -931
  18. package/src/core/CrossReferenceEngine.js +0 -624
  19. package/src/core/CurationEngine.js +0 -688
  20. package/src/core/DeprecationScheduler.js +0 -183
  21. package/src/core/LoadBearingDetector.js +0 -242
  22. package/src/core/NotificationService.js +0 -1032
  23. package/src/core/RapportOrchestrator.js +0 -632
  24. package/src/core/RelevanceDetector.js +0 -694
  25. package/src/core/StandardLifecycle.js +0 -244
  26. package/src/core/StandardsIngestion.js +0 -991
  27. package/src/core/TeamLoadBearingDetector.js +0 -431
  28. package/src/core/parsers/adrParser.js +0 -479
  29. package/src/core/parsers/cursorRulesParser.js +0 -564
  30. package/src/core/parsers/eslintParser.js +0 -439
  31. package/src/database/dbOperations.js +0 -105
  32. package/src/handlers/activity/activityGetMe.js +0 -98
  33. package/src/handlers/activity/activityGetTeam.js +0 -175
  34. package/src/handlers/admin/adminSetup.js +0 -216
  35. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  36. package/src/handlers/alerts/alertsGet.js +0 -250
  37. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  38. package/src/handlers/analytics/coachingGet.js +0 -361
  39. package/src/handlers/analytics/convergenceGet.js +0 -236
  40. package/src/handlers/analytics/developerScoreGet.js +0 -137
  41. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  42. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  43. package/src/handlers/collaborators/collaboratorList.js +0 -82
  44. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  45. package/src/handlers/collaborators/inviteAccept.js +0 -122
  46. package/src/handlers/company/companyUsersDelete.js +0 -141
  47. package/src/handlers/company/companyUsersGet.js +0 -90
  48. package/src/handlers/company/companyUsersPost.js +0 -267
  49. package/src/handlers/company/companyUsersPut.js +0 -76
  50. package/src/handlers/context/contextGet.js +0 -57
  51. package/src/handlers/context/invariantsGet.js +0 -74
  52. package/src/handlers/context/loopsGet.js +0 -82
  53. package/src/handlers/context/notesCreate.js +0 -74
  54. package/src/handlers/context/purposeGet.js +0 -78
  55. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  56. package/src/handlers/correlations/correlationsGet.js +0 -93
  57. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  58. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  59. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  60. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  61. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  62. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  63. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  64. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  65. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  66. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  67. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  68. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  69. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  70. package/src/handlers/github/githubConnectionStatus.js +0 -49
  71. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  72. package/src/handlers/github/githubOAuthCallback.js +0 -178
  73. package/src/handlers/github/githubOAuthStart.js +0 -59
  74. package/src/handlers/github/githubPatternsReview.js +0 -76
  75. package/src/handlers/github/githubReposList.js +0 -105
  76. package/src/handlers/health/healthGet.js +0 -55
  77. package/src/handlers/helpers/auditLogger.js +0 -201
  78. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  79. package/src/handlers/helpers/decisionFrames.js +0 -29
  80. package/src/handlers/helpers/errorHandler.js +0 -49
  81. package/src/handlers/helpers/index.js +0 -138
  82. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  83. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  84. package/src/handlers/helpers/predictiveCache.js +0 -51
  85. package/src/handlers/helpers/projectAccess.js +0 -88
  86. package/src/handlers/helpers/responseUtil.js +0 -55
  87. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  88. package/src/handlers/mcp/mcpHandler.js +0 -569
  89. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  90. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  91. package/src/handlers/notifications/getPreferences.js +0 -84
  92. package/src/handlers/notifications/sendNotification.js +0 -170
  93. package/src/handlers/notifications/updatePreferences.js +0 -316
  94. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  95. package/src/handlers/patterns/patternUsagePost.js +0 -182
  96. package/src/handlers/patterns/patternViolationPost.js +0 -185
  97. package/src/handlers/projects/projectCreate.js +0 -248
  98. package/src/handlers/projects/projectDelete.js +0 -82
  99. package/src/handlers/projects/projectGet.js +0 -95
  100. package/src/handlers/projects/projectUpdate.js +0 -117
  101. package/src/handlers/reports/aiLeverage.js +0 -210
  102. package/src/handlers/reports/engineeringInvestment.js +0 -132
  103. package/src/handlers/reports/riskForecast.js +0 -206
  104. package/src/handlers/reports/standardsRoi.js +0 -254
  105. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  106. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  107. package/src/handlers/scheduled/generateAlerts.js +0 -135
  108. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  109. package/src/handlers/scheduled/refreshActivity.js +0 -21
  110. package/src/handlers/scheduled/scanCompliance.js +0 -334
  111. package/src/handlers/sessions/sessionEndPost.js +0 -180
  112. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  113. package/src/handlers/standards/catalogGet.js +0 -185
  114. package/src/handlers/standards/catalogSync.js +0 -120
  115. package/src/handlers/standards/discoveriesGet.js +0 -89
  116. package/src/handlers/standards/projectStandardsGet.js +0 -129
  117. package/src/handlers/standards/projectStandardsPut.js +0 -151
  118. package/src/handlers/standards/standardsAuditGet.js +0 -65
  119. package/src/handlers/standards/standardsParseUpload.js +0 -149
  120. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  121. package/src/handlers/standards/standardsTransition.js +0 -161
  122. package/src/handlers/stripe/addonManagePost.js +0 -240
  123. package/src/handlers/stripe/billingPortalPost.js +0 -93
  124. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  125. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  126. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  127. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  128. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  129. package/src/handlers/stripe/webhookPost.js +0 -482
  130. package/src/handlers/user/apiTokenCreate.js +0 -71
  131. package/src/handlers/user/apiTokenList.js +0 -64
  132. package/src/handlers/user/userSplashAck.js +0 -91
  133. package/src/handlers/user/userSplashGet.js +0 -211
  134. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  135. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  136. package/src/handlers/users/userEntitlementsGet.js +0 -89
  137. package/src/handlers/users/userGet.js +0 -118
  138. package/src/handlers/users/userProfilePut.js +0 -77
  139. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,117 +0,0 @@
1
- /**
2
- * Project Update Handler
3
- * Updates project metadata
4
- *
5
- * PUT /api/projects
6
- * Body: { project_id, 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({ 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 { project_id: projectId, project_name, description, private: isPrivate, repo_url } = requestBody;
22
-
23
- if (!projectId) {
24
- return createErrorResponse(400, 'projectId is required');
25
- }
26
-
27
- // Check user has access to project
28
- const accessQuery = `
29
- SELECT
30
- p.project_id,
31
- p.company_id,
32
- pc.role,
33
- ue.admin as company_admin
34
- FROM rapport.projects p
35
- LEFT JOIN rapport.project_collaborators pc
36
- ON p.project_id = pc.project_id
37
- AND pc.email_address = $1
38
- LEFT JOIN rapport.user_entitlements ue
39
- ON ue.email_address = $1
40
- AND ue.company_id = p.company_id
41
- WHERE p.project_id = $2
42
- `;
43
- const accessCheck = await executeQuery(accessQuery, [email, projectId]);
44
-
45
- if (accessCheck.rowCount === 0) {
46
- return createErrorResponse(404, 'Project not found');
47
- }
48
-
49
- const access = accessCheck.rows[0];
50
-
51
- // Check permissions (owner, admin collaborator, or company admin)
52
- const hasAccess = access.role === 'owner' || access.company_admin === true;
53
- if (!hasAccess) {
54
- return createErrorResponse(403, 'Insufficient permissions to update project');
55
- }
56
-
57
- // Build update query dynamically
58
- const updates = [];
59
- const values = [];
60
- let paramIndex = 1;
61
-
62
- if (project_name !== undefined) {
63
- updates.push(`project_name = $${paramIndex++}`);
64
- values.push(project_name);
65
- }
66
- if (description !== undefined) {
67
- updates.push(`description = $${paramIndex++}`);
68
- values.push(description);
69
- }
70
- if (isPrivate !== undefined) {
71
- updates.push(`private = $${paramIndex++}`);
72
- values.push(isPrivate);
73
- }
74
- if (repo_url !== undefined) {
75
- updates.push(`repo_url = $${paramIndex++}`);
76
- values.push(repo_url);
77
- }
78
-
79
- if (updates.length === 0) {
80
- return createErrorResponse(400, 'No fields to update');
81
- }
82
-
83
- values.push(projectId);
84
-
85
- const query = `
86
- UPDATE rapport.projects
87
- SET ${updates.join(', ')}, updated_at = NOW()
88
- WHERE project_id = $${paramIndex}
89
- RETURNING
90
- project_id,
91
- company_id,
92
- project_name,
93
- description,
94
- private,
95
- repo_url,
96
- updated_at
97
- `;
98
-
99
- const result = await executeQuery(query, values);
100
-
101
- return createSuccessResponse(
102
- { Records: result.rows },
103
- 'Project updated successfully',
104
- {
105
- Total_Records: result.rowCount,
106
- Request_ID,
107
- Timestamp: new Date().toISOString()
108
- }
109
- );
110
-
111
- } catch (error) {
112
- console.error('Handler Error:', error);
113
- return handleError(error);
114
- }
115
- }
116
-
117
- exports.handler = wrapHandler(updateProject);
@@ -1,210 +0,0 @@
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
24
- const accessCheck = await executeQuery(`
25
- SELECT ue.company_id
26
- FROM rapport.user_entitlements ue
27
- WHERE ue.email_address = $1
28
- AND (ue.admin = true OR ue.manager = true)
29
- ${companyId ? 'AND ue.company_id = $2' : ''}
30
- 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
- const multiplier = calculateMultiplier(aiAssistedDevs, nonAiDevs);
110
- const totalCommits = devMetrics.rows.reduce((sum, d) => sum + parseInt(d.total_commits || 0), 0);
111
- const aiCommits = aiAssistedDevs.reduce((sum, d) => sum + parseInt(d.total_commits || 0), 0);
112
-
113
- return createSuccessResponse({
114
- report_type: 'ai_leverage',
115
- period: period,
116
- period_start: periodStart.toISOString(),
117
- period_end: new Date().toISOString(),
118
- summary: {
119
- total_sessions: parseInt(sessionData.total_sessions) || 0,
120
- unique_ai_users: aiAssistedDevs.length,
121
- avg_session_minutes: parseFloat(sessionData.avg_session_minutes || 0).toFixed(1),
122
- ai_assisted_commits: aiCommits,
123
- total_commits: totalCommits,
124
- ai_commit_percentage: totalCommits > 0 ? ((aiCommits / totalCommits) * 100).toFixed(1) : '0',
125
- productivity_multiplier: multiplier.available ? multiplier.value : '1.0'
126
- },
127
- comparison: {
128
- ai_assisted: {
129
- developer_count: aiAssistedDevs.length,
130
- avg_commits_per_dev: calcAvg(aiAssistedDevs, 'total_commits').toFixed(1),
131
- avg_lines_per_dev: calcAvg(aiAssistedDevs, 'lines_added').toFixed(0),
132
- avg_compliance: calcAvg(aiAssistedDevs, 'avg_compliance').toFixed(1)
133
- },
134
- non_ai: {
135
- developer_count: nonAiDevs.length,
136
- avg_commits_per_dev: calcAvg(nonAiDevs, 'total_commits').toFixed(1),
137
- avg_lines_per_dev: calcAvg(nonAiDevs, 'lines_added').toFixed(0),
138
- avg_compliance: calcAvg(nonAiDevs, 'avg_compliance').toFixed(1)
139
- }
140
- },
141
- standards_effectiveness: [],
142
- insights: generateInsights(aiAssistedDevs, nonAiDevs, sessionData)
143
- });
144
- });
145
-
146
- function calculateMultiplier(aiDevs, nonAiDevs) {
147
- if (aiDevs.length === 0 || nonAiDevs.length === 0) {
148
- return { available: false, message: 'Need both AI and non-AI developers to calculate' };
149
- }
150
-
151
- const aiAvgCommits = aiDevs.reduce((s, d) => s + parseFloat(d.total_commits || 0), 0) / aiDevs.length;
152
- const nonAiAvgCommits = nonAiDevs.reduce((s, d) => s + parseFloat(d.total_commits || 0), 0) / nonAiDevs.length;
153
-
154
- if (nonAiAvgCommits === 0) {
155
- return { available: false, message: 'Non-AI baseline has no commits' };
156
- }
157
-
158
- return {
159
- available: true,
160
- value: (aiAvgCommits / nonAiAvgCommits).toFixed(2),
161
- interpretation: aiAvgCommits > nonAiAvgCommits
162
- ? 'AI-assisted developers are more productive'
163
- : 'Non-AI developers are more productive'
164
- };
165
- }
166
-
167
- function generateInsights(aiDevs, nonAiDevs, sessionData) {
168
- const insights = [];
169
-
170
- const totalSessions = parseInt(sessionData.total_sessions) || 0;
171
-
172
- if (totalSessions === 0) {
173
- insights.push({
174
- type: 'info',
175
- message: 'No Claude Code sessions detected. Install Claude Code CLI to start tracking AI-assisted development.'
176
- });
177
- return insights;
178
- }
179
-
180
- if (aiDevs.length > 0) {
181
- const aiAvgCompliance = aiDevs.reduce((s, d) => s + parseFloat(d.avg_compliance || 0), 0) / aiDevs.length;
182
- const nonAiAvgCompliance = nonAiDevs.length > 0
183
- ? nonAiDevs.reduce((s, d) => s + parseFloat(d.avg_compliance || 0), 0) / nonAiDevs.length
184
- : 0;
185
-
186
- if (aiAvgCompliance > nonAiAvgCompliance + 10) {
187
- insights.push({
188
- type: 'positive',
189
- message: `AI-assisted developers have ${(aiAvgCompliance - nonAiAvgCompliance).toFixed(0)}% higher compliance scores`
190
- });
191
- }
192
- }
193
-
194
- return insights;
195
- }
196
-
197
- function parsePeriod(period) {
198
- const match = period.match(/^(\d+)([dwm])$/);
199
- if (!match) return 30;
200
-
201
- const [, num, unit] = match;
202
- const n = parseInt(num);
203
-
204
- switch (unit) {
205
- case 'd': return n;
206
- case 'w': return n * 7;
207
- case 'm': return n * 30;
208
- default: return 30;
209
- }
210
- }
@@ -1,132 +0,0 @@
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
24
- const accessCheck = await executeQuery(`
25
- SELECT ue.company_id
26
- FROM rapport.user_entitlements ue
27
- WHERE ue.email_address = $1
28
- AND (ue.admin = true OR ue.manager = true)
29
- ${companyId ? 'AND ue.company_id = $2' : ''}
30
- 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
- c.repo_name 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.commit_timestamp >= $2
55
- GROUP BY c.repo_name
56
- )
57
- SELECT
58
- module,
59
- commit_count,
60
- lines_added,
61
- lines_removed,
62
- contributors,
63
- ROUND(commit_count * 100.0 / NULLIF(SUM(commit_count) OVER (), 0), 1) as commit_percentage,
64
- ROUND((lines_added + lines_removed) * 100.0 / NULLIF(SUM(lines_added + lines_removed) OVER (), 0), 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.commit_timestamp >= $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
- }
@@ -1,206 +0,0 @@
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
23
- const accessCheck = await executeQuery(`
24
- SELECT ue.company_id
25
- FROM rapport.user_entitlements 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
- const riskLevel = riskScore >= 70 ? 'critical' : riskScore >= 40 ? 'elevated' : 'normal';
118
-
119
- return createSuccessResponse({
120
- report_type: 'risk_forecast',
121
- generated_at: new Date().toISOString(),
122
- overall_risk_score: riskScore,
123
- risk_level: riskLevel,
124
- summary: {
125
- inactive_developers: 0,
126
- high_velocity_devs: burnoutCount,
127
- critical_silos: criticalSilos,
128
- stale_prs: staleCount,
129
- active_alerts: 0,
130
- high_churn_files: 0
131
- },
132
- risks: {
133
- knowledge_silos: silos.rows,
134
- inactive_developers: [],
135
- burnout_risk: burnoutRisk.rows.map(b => ({
136
- email: b.developer_email,
137
- name: b.developer_email,
138
- commits_7d: 0,
139
- weekend_commits: parseInt(b.weekend_commits) || 0,
140
- after_hours: parseInt(b.after_hours_commits) || 0
141
- })),
142
- review_bottleneck: stalePRs.rows.map(pr => ({
143
- repo: '',
144
- pr_number: 0,
145
- title: pr.title,
146
- author: pr.author_email,
147
- days_open: parseInt(pr.days_open) || 0
148
- })),
149
- high_churn_files: []
150
- },
151
- active_alerts: [],
152
- recommendations: generateRecommendations(silos.rows, stalePRs.rows, burnoutRisk.rows)
153
- });
154
- });
155
-
156
- function generateRecommendations(silos, stalePRs, burnout) {
157
- const recommendations = [];
158
-
159
- // Knowledge silo recommendations
160
- const criticalSilos = silos.filter(s => s.risk_level === 'critical');
161
- if (criticalSilos.length > 0) {
162
- recommendations.push({
163
- priority: 'high',
164
- category: 'Knowledge Sharing',
165
- message: `${criticalSilos.length} critical knowledge silo(s) detected. Only one developer knows these areas.`,
166
- action: 'Schedule pair programming or code review sessions to spread knowledge.',
167
- modules: criticalSilos.map(s => s.module_path)
168
- });
169
- }
170
-
171
- // Stale PR recommendations
172
- if (stalePRs.length > 0) {
173
- const veryStale = stalePRs.filter(pr => parseInt(pr.days_open) > 14);
174
- if (veryStale.length > 0) {
175
- recommendations.push({
176
- priority: 'medium',
177
- category: 'Code Review',
178
- message: `${veryStale.length} PR(s) open for more than 2 weeks.`,
179
- action: 'Review and either merge, close, or request updates on these PRs.',
180
- prs: veryStale.map(pr => pr.title)
181
- });
182
- }
183
- }
184
-
185
- // Burnout recommendations
186
- if (burnout.length > 0) {
187
- recommendations.push({
188
- priority: 'high',
189
- category: 'Team Health',
190
- message: `${burnout.length} developer(s) showing potential burnout indicators (frequent after-hours/weekend commits).`,
191
- action: 'Check in with these team members about workload and work-life balance.',
192
- developers: burnout.map(b => b.developer_email)
193
- });
194
- }
195
-
196
- if (recommendations.length === 0) {
197
- recommendations.push({
198
- priority: 'info',
199
- category: 'Status',
200
- message: 'No significant risks detected.',
201
- action: 'Continue monitoring and maintaining healthy development practices.'
202
- });
203
- }
204
-
205
- return recommendations;
206
- }