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