@equilateral_ai/mindmeld 3.5.3 → 4.0.2

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 (138) hide show
  1. package/hooks/session-start.js +312 -85
  2. package/package.json +21 -13
  3. package/scripts/init-project.js +9 -23
  4. package/scripts/repo-analyzer.js +118 -2
  5. package/src/client/dbShim.js +16 -0
  6. package/src/core/AuthManager.js +3 -2
  7. package/src/handlers/helpers/dbOperations.js +9 -46
  8. package/src/index.js +2 -217
  9. package/src/utils/piiMask.js +16 -0
  10. package/scripts/inject.js +0 -409
  11. package/scripts/mcp-bridge.js +0 -220
  12. package/scripts/standards.js +0 -285
  13. package/src/collaboration/CollaborationPrompt.js +0 -460
  14. package/src/core/AlertEngine.js +0 -813
  15. package/src/core/AlertNotifier.js +0 -363
  16. package/src/core/CorrelationAnalyzer.js +0 -931
  17. package/src/core/CrossReferenceEngine.js +0 -624
  18. package/src/core/CurationEngine.js +0 -688
  19. package/src/core/DeprecationScheduler.js +0 -183
  20. package/src/core/LoadBearingDetector.js +0 -242
  21. package/src/core/NotificationService.js +0 -1032
  22. package/src/core/RapportOrchestrator.js +0 -632
  23. package/src/core/RelevanceDetector.js +0 -694
  24. package/src/core/StandardLifecycle.js +0 -244
  25. package/src/core/StandardsIngestion.js +0 -991
  26. package/src/core/TeamLoadBearingDetector.js +0 -431
  27. package/src/core/parsers/adrParser.js +0 -479
  28. package/src/core/parsers/cursorRulesParser.js +0 -564
  29. package/src/core/parsers/eslintParser.js +0 -439
  30. package/src/database/dbOperations.js +0 -105
  31. package/src/handlers/activity/activityGetMe.js +0 -98
  32. package/src/handlers/activity/activityGetTeam.js +0 -175
  33. package/src/handlers/admin/adminSetup.js +0 -216
  34. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  35. package/src/handlers/alerts/alertsGet.js +0 -250
  36. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  37. package/src/handlers/analytics/coachingGet.js +0 -361
  38. package/src/handlers/analytics/convergenceGet.js +0 -236
  39. package/src/handlers/analytics/developerScoreGet.js +0 -137
  40. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  41. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  42. package/src/handlers/collaborators/collaboratorList.js +0 -82
  43. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  44. package/src/handlers/collaborators/inviteAccept.js +0 -122
  45. package/src/handlers/company/companyUsersDelete.js +0 -141
  46. package/src/handlers/company/companyUsersGet.js +0 -90
  47. package/src/handlers/company/companyUsersPost.js +0 -267
  48. package/src/handlers/company/companyUsersPut.js +0 -76
  49. package/src/handlers/context/contextGet.js +0 -57
  50. package/src/handlers/context/invariantsGet.js +0 -74
  51. package/src/handlers/context/loopsGet.js +0 -82
  52. package/src/handlers/context/notesCreate.js +0 -74
  53. package/src/handlers/context/purposeGet.js +0 -78
  54. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  55. package/src/handlers/correlations/correlationsGet.js +0 -93
  56. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  57. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  58. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  59. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  60. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  61. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  62. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  63. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  64. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  65. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  66. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  67. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  68. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  69. package/src/handlers/github/githubConnectionStatus.js +0 -49
  70. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  71. package/src/handlers/github/githubOAuthCallback.js +0 -178
  72. package/src/handlers/github/githubOAuthStart.js +0 -59
  73. package/src/handlers/github/githubPatternsReview.js +0 -76
  74. package/src/handlers/github/githubReposList.js +0 -105
  75. package/src/handlers/health/healthGet.js +0 -55
  76. package/src/handlers/helpers/auditLogger.js +0 -201
  77. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  78. package/src/handlers/helpers/decisionFrames.js +0 -29
  79. package/src/handlers/helpers/errorHandler.js +0 -49
  80. package/src/handlers/helpers/index.js +0 -138
  81. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  82. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  83. package/src/handlers/helpers/predictiveCache.js +0 -51
  84. package/src/handlers/helpers/projectAccess.js +0 -88
  85. package/src/handlers/helpers/responseUtil.js +0 -55
  86. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  87. package/src/handlers/mcp/mcpHandler.js +0 -569
  88. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  89. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  90. package/src/handlers/notifications/getPreferences.js +0 -84
  91. package/src/handlers/notifications/sendNotification.js +0 -170
  92. package/src/handlers/notifications/updatePreferences.js +0 -316
  93. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  94. package/src/handlers/patterns/patternUsagePost.js +0 -182
  95. package/src/handlers/patterns/patternViolationPost.js +0 -185
  96. package/src/handlers/projects/projectCreate.js +0 -248
  97. package/src/handlers/projects/projectDelete.js +0 -82
  98. package/src/handlers/projects/projectGet.js +0 -95
  99. package/src/handlers/projects/projectUpdate.js +0 -117
  100. package/src/handlers/reports/aiLeverage.js +0 -210
  101. package/src/handlers/reports/engineeringInvestment.js +0 -132
  102. package/src/handlers/reports/riskForecast.js +0 -206
  103. package/src/handlers/reports/standardsRoi.js +0 -254
  104. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  105. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  106. package/src/handlers/scheduled/generateAlerts.js +0 -135
  107. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  108. package/src/handlers/scheduled/refreshActivity.js +0 -21
  109. package/src/handlers/scheduled/scanCompliance.js +0 -334
  110. package/src/handlers/sessions/sessionEndPost.js +0 -180
  111. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  112. package/src/handlers/standards/catalogGet.js +0 -185
  113. package/src/handlers/standards/catalogSync.js +0 -120
  114. package/src/handlers/standards/discoveriesGet.js +0 -89
  115. package/src/handlers/standards/projectStandardsGet.js +0 -129
  116. package/src/handlers/standards/projectStandardsPut.js +0 -151
  117. package/src/handlers/standards/standardsAuditGet.js +0 -65
  118. package/src/handlers/standards/standardsParseUpload.js +0 -149
  119. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  120. package/src/handlers/standards/standardsTransition.js +0 -161
  121. package/src/handlers/stripe/addonManagePost.js +0 -240
  122. package/src/handlers/stripe/billingPortalPost.js +0 -93
  123. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  124. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  125. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  126. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  127. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  128. package/src/handlers/stripe/webhookPost.js +0 -482
  129. package/src/handlers/user/apiTokenCreate.js +0 -71
  130. package/src/handlers/user/apiTokenList.js +0 -64
  131. package/src/handlers/user/userSplashAck.js +0 -91
  132. package/src/handlers/user/userSplashGet.js +0 -211
  133. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  134. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  135. package/src/handlers/users/userEntitlementsGet.js +0 -89
  136. package/src/handlers/users/userGet.js +0 -118
  137. package/src/handlers/users/userProfilePut.js +0 -77
  138. 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
- }