@equilateral_ai/mindmeld 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-start.js +636 -42
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +12 -8
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +46 -51
|
@@ -52,13 +52,13 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
|
|
|
52
52
|
// Check authorization (only admins and managers can send notifications)
|
|
53
53
|
const authCheck = await executeQuery(`
|
|
54
54
|
SELECT
|
|
55
|
-
ue.
|
|
56
|
-
ue.
|
|
57
|
-
u.
|
|
58
|
-
ue.
|
|
59
|
-
FROM
|
|
60
|
-
JOIN
|
|
61
|
-
WHERE ue.
|
|
55
|
+
ue.admin,
|
|
56
|
+
ue.manager,
|
|
57
|
+
u.super_admin,
|
|
58
|
+
ue.company_id
|
|
59
|
+
FROM rapport.user_entitlements ue
|
|
60
|
+
JOIN rapport.users u ON ue.email_address = u.email_address
|
|
61
|
+
WHERE ue.email_address = $1
|
|
62
62
|
`, [email]);
|
|
63
63
|
|
|
64
64
|
if (authCheck.rowCount === 0) {
|
|
@@ -66,7 +66,7 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const userRole = authCheck.rows[0];
|
|
69
|
-
const isAuthorized = userRole.
|
|
69
|
+
const isAuthorized = userRole.super_admin || userRole.admin || userRole.manager;
|
|
70
70
|
|
|
71
71
|
if (!isAuthorized) {
|
|
72
72
|
return createErrorResponse(403, 'Only admins and managers can send notifications');
|
|
@@ -89,18 +89,18 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
|
|
|
89
89
|
} else if (recipients === 'all_admins') {
|
|
90
90
|
// All company admins
|
|
91
91
|
const admins = await executeQuery(`
|
|
92
|
-
SELECT ue.
|
|
93
|
-
FROM
|
|
94
|
-
WHERE ue.
|
|
95
|
-
`, [userRole.
|
|
92
|
+
SELECT ue.email_address
|
|
93
|
+
FROM rapport.user_entitlements ue
|
|
94
|
+
WHERE ue.company_id = $1 AND ue.admin = true
|
|
95
|
+
`, [userRole.company_id]);
|
|
96
96
|
recipientEmails = admins.rows.map(r => r.email_address);
|
|
97
97
|
} else if (recipients === 'all_managers') {
|
|
98
98
|
// All company managers
|
|
99
99
|
const managers = await executeQuery(`
|
|
100
|
-
SELECT ue.
|
|
101
|
-
FROM
|
|
102
|
-
WHERE ue.
|
|
103
|
-
`, [userRole.
|
|
100
|
+
SELECT ue.email_address
|
|
101
|
+
FROM rapport.user_entitlements ue
|
|
102
|
+
WHERE ue.company_id = $1 AND (ue.manager = true OR ue.admin = true)
|
|
103
|
+
`, [userRole.company_id]);
|
|
104
104
|
recipientEmails = managers.rows.map(r => r.email_address);
|
|
105
105
|
} else {
|
|
106
106
|
return createErrorResponse(400, 'Invalid recipients format');
|
|
@@ -118,8 +118,8 @@ exports.handler = wrapHandler(async ({ requestContext, body }) => {
|
|
|
118
118
|
// Get preferences for all recipients
|
|
119
119
|
const prefsQuery = await executeQuery(`
|
|
120
120
|
SELECT email_address, rapport.get_notification_preferences(email_address) as preferences
|
|
121
|
-
FROM
|
|
122
|
-
WHERE
|
|
121
|
+
FROM rapport.users
|
|
122
|
+
WHERE email_address = ANY($1)
|
|
123
123
|
`, [recipientEmails]);
|
|
124
124
|
|
|
125
125
|
const prefsMap = new Map(prefsQuery.rows.map(r => [r.email_address, r.preferences]));
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Evaluate Promotion Handler
|
|
3
|
+
* Checks if a pattern meets promotion thresholds for becoming a standard
|
|
4
|
+
*
|
|
5
|
+
* POST /api/patterns/evaluate-promotion
|
|
6
|
+
* Body: { pattern, project_id, evaluate_only }
|
|
7
|
+
*
|
|
8
|
+
* Called by: pre-compact.js hook (evaluateForPromotion method)
|
|
9
|
+
*
|
|
10
|
+
* Promotion Criteria:
|
|
11
|
+
* - handoff_count >= 10 (used 10+ times)
|
|
12
|
+
* - success_rate >= 0.70 (70% success)
|
|
13
|
+
* - developer_count >= 3 (used by 3+ developers)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Evaluate pattern for promotion to standard
|
|
20
|
+
*/
|
|
21
|
+
async function evaluatePatternPromotion({ body: requestBody = {}, requestContext }) {
|
|
22
|
+
try {
|
|
23
|
+
const Request_ID = requestContext?.requestId || 'unknown';
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
pattern,
|
|
27
|
+
project_id,
|
|
28
|
+
evaluate_only = true
|
|
29
|
+
} = requestBody;
|
|
30
|
+
|
|
31
|
+
// Validate required fields
|
|
32
|
+
if (!pattern || !pattern.element) {
|
|
33
|
+
return createErrorResponse(400, 'pattern.element is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate pattern_id from element
|
|
37
|
+
const patternId = pattern.pattern_id || `pat_${pattern.element.toLowerCase().replace(/\s+/g, '_').substring(0, 50)}`;
|
|
38
|
+
|
|
39
|
+
// Look up pattern in rapport.patterns
|
|
40
|
+
const patternQuery = `
|
|
41
|
+
SELECT
|
|
42
|
+
p.pattern_id,
|
|
43
|
+
p.intent,
|
|
44
|
+
p.maturity,
|
|
45
|
+
p.handoff_count,
|
|
46
|
+
p.successful_handoffs,
|
|
47
|
+
p.failed_handoffs,
|
|
48
|
+
p.discovered_at,
|
|
49
|
+
p.last_used
|
|
50
|
+
FROM rapport.patterns p
|
|
51
|
+
WHERE p.pattern_id = $1
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const patternResult = await executeQuery(patternQuery, [patternId]);
|
|
55
|
+
|
|
56
|
+
if (patternResult.rows.length === 0) {
|
|
57
|
+
return createSuccessResponse(
|
|
58
|
+
{
|
|
59
|
+
Records: [{
|
|
60
|
+
pattern_id: patternId,
|
|
61
|
+
eligible: false,
|
|
62
|
+
reason: 'not_found',
|
|
63
|
+
message: 'Pattern not found in patterns table'
|
|
64
|
+
}]
|
|
65
|
+
},
|
|
66
|
+
'Pattern not found',
|
|
67
|
+
{ Total_Records: 0, Request_ID, Timestamp: new Date().toISOString() }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pat = patternResult.rows[0];
|
|
72
|
+
|
|
73
|
+
// Query usage metrics
|
|
74
|
+
const metricsQuery = `
|
|
75
|
+
SELECT
|
|
76
|
+
COUNT(DISTINCT pu.email_address) as developer_count,
|
|
77
|
+
COUNT(DISTINCT pu.session_id) as session_count,
|
|
78
|
+
COUNT(*) FILTER (WHERE pu.success = true) as successes,
|
|
79
|
+
COUNT(*) as total_uses
|
|
80
|
+
FROM rapport.pattern_usage pu
|
|
81
|
+
WHERE pu.pattern_id = $1
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const metricsResult = await executeQuery(metricsQuery, [patternId]);
|
|
85
|
+
const metrics = metricsResult.rows[0];
|
|
86
|
+
|
|
87
|
+
const handoffCount = parseInt(pat.handoff_count) || 0;
|
|
88
|
+
const successRate = handoffCount > 0
|
|
89
|
+
? (parseInt(pat.successful_handoffs) || 0) / handoffCount
|
|
90
|
+
: 0;
|
|
91
|
+
const developerCount = parseInt(metrics.developer_count) || 0;
|
|
92
|
+
const sessionCount = parseInt(metrics.session_count) || 0;
|
|
93
|
+
|
|
94
|
+
// Check promotion thresholds
|
|
95
|
+
const eligible =
|
|
96
|
+
handoffCount >= 10 &&
|
|
97
|
+
successRate >= 0.70 &&
|
|
98
|
+
developerCount >= 3;
|
|
99
|
+
|
|
100
|
+
const response = {
|
|
101
|
+
pattern_id: patternId,
|
|
102
|
+
eligible: eligible,
|
|
103
|
+
metrics: {
|
|
104
|
+
handoff_count: handoffCount,
|
|
105
|
+
success_rate: parseFloat(successRate.toFixed(3)),
|
|
106
|
+
developer_count: developerCount,
|
|
107
|
+
session_count: sessionCount,
|
|
108
|
+
maturity: pat.maturity,
|
|
109
|
+
success_correlation: parseFloat(successRate.toFixed(3))
|
|
110
|
+
},
|
|
111
|
+
thresholds: {
|
|
112
|
+
min_handoffs: 10,
|
|
113
|
+
min_success_rate: 0.70,
|
|
114
|
+
min_developers: 3
|
|
115
|
+
},
|
|
116
|
+
proposed_category: pattern.category || null
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// If eligible and not evaluate_only, create curation candidate
|
|
120
|
+
if (eligible && !evaluate_only) {
|
|
121
|
+
const candidateQuery = `
|
|
122
|
+
INSERT INTO rapport.curation_candidates (
|
|
123
|
+
pattern_id,
|
|
124
|
+
proposed_category,
|
|
125
|
+
evidence,
|
|
126
|
+
status,
|
|
127
|
+
created_at
|
|
128
|
+
) VALUES ($1, $2, $3, 'pending', NOW())
|
|
129
|
+
ON CONFLICT (pattern_id) WHERE status = 'pending'
|
|
130
|
+
DO UPDATE SET
|
|
131
|
+
evidence = EXCLUDED.evidence,
|
|
132
|
+
created_at = NOW()
|
|
133
|
+
RETURNING candidate_id
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
const evidence = {
|
|
137
|
+
handoff_count: handoffCount,
|
|
138
|
+
success_rate: successRate,
|
|
139
|
+
developer_count: developerCount,
|
|
140
|
+
session_count: sessionCount,
|
|
141
|
+
maturity: pat.maturity,
|
|
142
|
+
evaluated_at: new Date().toISOString()
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const candidateResult = await executeQuery(candidateQuery, [
|
|
147
|
+
patternId,
|
|
148
|
+
pattern.category || 'uncategorized',
|
|
149
|
+
JSON.stringify(evidence)
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
if (candidateResult.rows.length > 0) {
|
|
153
|
+
response.candidate_id = candidateResult.rows[0].candidate_id;
|
|
154
|
+
}
|
|
155
|
+
} catch (candidateError) {
|
|
156
|
+
// Candidate creation failed — still return eligibility result
|
|
157
|
+
console.error('[patternEvaluatePromotionPost] Candidate creation failed:', candidateError.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return createSuccessResponse(
|
|
162
|
+
{ Records: [response] },
|
|
163
|
+
eligible ? 'Pattern eligible for promotion' : 'Pattern does not meet promotion criteria',
|
|
164
|
+
{ Total_Records: 1, Request_ID, Timestamp: new Date().toISOString() }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Handler Error:', error);
|
|
169
|
+
return handleError(error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
exports.handler = wrapHandler(evaluatePatternPromotion);
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, checkSubscriptionLimits } = require('./helpers');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const https = require('https');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Create project
|
|
@@ -27,24 +29,24 @@ async function createProject({ body: requestBody = {}, requestContext }) {
|
|
|
27
29
|
|
|
28
30
|
// Check user has admin access to company
|
|
29
31
|
const adminQuery = `
|
|
30
|
-
SELECT ue.
|
|
31
|
-
FROM
|
|
32
|
-
JOIN
|
|
33
|
-
WHERE ue.
|
|
34
|
-
AND ue.
|
|
32
|
+
SELECT ue.admin, c.company_name
|
|
33
|
+
FROM rapport.user_entitlements ue
|
|
34
|
+
JOIN rapport.companies c ON ue.company_id = c.company_id
|
|
35
|
+
WHERE ue.email_address = $1
|
|
36
|
+
AND ue.company_id = $2
|
|
35
37
|
`;
|
|
36
38
|
const adminCheck = await executeQuery(adminQuery, [email, Company_ID]);
|
|
37
39
|
|
|
38
|
-
if (adminCheck.rowCount === 0 || !adminCheck.rows[0].
|
|
40
|
+
if (adminCheck.rowCount === 0 || !adminCheck.rows[0].admin) {
|
|
39
41
|
return createErrorResponse(403, 'Admin access required to create projects');
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// Check project limit for subscription tier
|
|
43
45
|
const clientQuery = `
|
|
44
46
|
SELECT c.subscription_tier
|
|
45
|
-
FROM
|
|
46
|
-
JOIN
|
|
47
|
-
WHERE ue.
|
|
47
|
+
FROM rapport.clients c
|
|
48
|
+
JOIN rapport.user_entitlements ue ON c.client_id = ue.client_id
|
|
49
|
+
WHERE ue.email_address = $1 AND ue.company_id = $2
|
|
48
50
|
`;
|
|
49
51
|
const clientResult = await executeQuery(clientQuery, [email, Company_ID]);
|
|
50
52
|
const subscriptionTier = clientResult.rows[0]?.subscription_tier || 'free';
|
|
@@ -112,8 +114,18 @@ async function createProject({ body: requestBody = {}, requestContext }) {
|
|
|
112
114
|
`;
|
|
113
115
|
await executeQuery(collabQuery, [project_id, email]);
|
|
114
116
|
|
|
117
|
+
// Auto-create GitHub webhook if repo_url is a GitHub URL
|
|
118
|
+
let webhookCreated = false;
|
|
119
|
+
if (repo_url && repo_url.includes('github.com')) {
|
|
120
|
+
try {
|
|
121
|
+
webhookCreated = await createGitHubWebhook(email, repo_url, Company_ID);
|
|
122
|
+
} catch (webhookErr) {
|
|
123
|
+
console.warn('Webhook creation failed (non-blocking):', webhookErr.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
return createSuccessResponse(
|
|
116
|
-
{ Records: result.rows },
|
|
128
|
+
{ Records: result.rows, webhook_created: webhookCreated },
|
|
117
129
|
'Project created successfully',
|
|
118
130
|
{
|
|
119
131
|
Total_Records: result.rowCount,
|
|
@@ -131,4 +143,106 @@ async function createProject({ body: requestBody = {}, requestContext }) {
|
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Create GitHub webhook on a repo when a project is connected
|
|
148
|
+
* Uses the customer's stored GitHub OAuth token and a per-repo secret
|
|
149
|
+
*/
|
|
150
|
+
async function createGitHubWebhook(email, repoUrl, companyId) {
|
|
151
|
+
// Extract owner/repo from URL
|
|
152
|
+
const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
153
|
+
if (!match) {
|
|
154
|
+
console.log('Could not parse GitHub owner/repo from:', repoUrl);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const [, owner, repo] = match;
|
|
158
|
+
|
|
159
|
+
// Get the user's GitHub token
|
|
160
|
+
const connResult = await executeQuery(`
|
|
161
|
+
SELECT access_token_encrypted FROM rapport.github_connections
|
|
162
|
+
WHERE email_address = $1 AND revoked = FALSE
|
|
163
|
+
`, [email]);
|
|
164
|
+
|
|
165
|
+
if (connResult.rowCount === 0) {
|
|
166
|
+
console.log('No GitHub connection for', email);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
|
171
|
+
if (!encryptionKey) return false;
|
|
172
|
+
|
|
173
|
+
const accessToken = decryptToken(connResult.rows[0].access_token_encrypted, encryptionKey);
|
|
174
|
+
|
|
175
|
+
// Generate per-repo webhook secret
|
|
176
|
+
const webhookSecret = crypto.randomBytes(32).toString('hex');
|
|
177
|
+
const webhookUrl = process.env.WEBHOOK_URL || 'https://api.mindmeld.dev/api/webhooks/github';
|
|
178
|
+
|
|
179
|
+
// Create webhook via GitHub API
|
|
180
|
+
const payload = JSON.stringify({
|
|
181
|
+
name: 'web',
|
|
182
|
+
active: true,
|
|
183
|
+
events: ['push', 'pull_request'],
|
|
184
|
+
config: {
|
|
185
|
+
url: webhookUrl,
|
|
186
|
+
content_type: 'json',
|
|
187
|
+
secret: webhookSecret,
|
|
188
|
+
insecure_ssl: '0'
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const response = await new Promise((resolve, reject) => {
|
|
193
|
+
const req = https.request({
|
|
194
|
+
hostname: 'api.github.com',
|
|
195
|
+
path: `/repos/${owner}/${repo}/hooks`,
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: {
|
|
198
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
199
|
+
'User-Agent': 'MindMeld-App',
|
|
200
|
+
'Accept': 'application/vnd.github+json',
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
'Content-Length': Buffer.byteLength(payload)
|
|
203
|
+
}
|
|
204
|
+
}, (res) => {
|
|
205
|
+
let data = '';
|
|
206
|
+
res.on('data', chunk => data += chunk);
|
|
207
|
+
res.on('end', () => {
|
|
208
|
+
try { resolve({ statusCode: res.statusCode, body: JSON.parse(data) }); }
|
|
209
|
+
catch (e) { resolve({ statusCode: res.statusCode, body: data }); }
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
req.on('error', reject);
|
|
213
|
+
req.write(payload);
|
|
214
|
+
req.end();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (response.statusCode !== 201) {
|
|
218
|
+
console.warn(`GitHub webhook creation returned ${response.statusCode}:`, JSON.stringify(response.body).substring(0, 200));
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const githubWebhookId = response.body.id;
|
|
223
|
+
|
|
224
|
+
// Register repo in git_repositories with per-repo webhook secret
|
|
225
|
+
// Unique constraint is on (company_id, repo_url)
|
|
226
|
+
await executeQuery(`
|
|
227
|
+
INSERT INTO rapport.git_repositories (
|
|
228
|
+
repo_id, repo_name, repo_url, company_id, webhook_secret, created_at, updated_at
|
|
229
|
+
) VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW())
|
|
230
|
+
ON CONFLICT (company_id, repo_url) DO UPDATE SET
|
|
231
|
+
webhook_secret = EXCLUDED.webhook_secret,
|
|
232
|
+
updated_at = NOW()
|
|
233
|
+
`, [`${owner}/${repo}`, repoUrl, companyId, webhookSecret]);
|
|
234
|
+
|
|
235
|
+
console.log(`Created GitHub webhook ${githubWebhookId} on ${owner}/${repo}`);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function decryptToken(encryptedData, key) {
|
|
240
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
241
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
242
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
|
243
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
244
|
+
decrypted += decipher.final('utf8');
|
|
245
|
+
return decrypted;
|
|
246
|
+
}
|
|
247
|
+
|
|
134
248
|
exports.handler = wrapHandler(createProject);
|
|
@@ -29,14 +29,14 @@ async function deleteProject({ queryStringParameters: queryParams = {}, requestC
|
|
|
29
29
|
p.project_id,
|
|
30
30
|
p.company_id,
|
|
31
31
|
pc.role,
|
|
32
|
-
ue.
|
|
32
|
+
ue.admin as company_admin
|
|
33
33
|
FROM rapport.projects p
|
|
34
34
|
LEFT JOIN rapport.project_collaborators pc
|
|
35
35
|
ON p.project_id = pc.project_id
|
|
36
36
|
AND pc.email_address = $1
|
|
37
|
-
LEFT JOIN
|
|
38
|
-
ON ue.
|
|
39
|
-
AND ue.
|
|
37
|
+
LEFT JOIN rapport.user_entitlements ue
|
|
38
|
+
ON ue.email_address = $1
|
|
39
|
+
AND ue.company_id = p.company_id
|
|
40
40
|
WHERE p.project_id = $2
|
|
41
41
|
`;
|
|
42
42
|
const accessCheck = await executeQuery(accessQuery, [email, projectId]);
|
|
@@ -20,10 +20,10 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
|
|
|
20
20
|
|
|
21
21
|
// Get user's company access
|
|
22
22
|
const entitlementQuery = `
|
|
23
|
-
SELECT ue.
|
|
24
|
-
FROM
|
|
25
|
-
JOIN
|
|
26
|
-
WHERE ue.
|
|
23
|
+
SELECT ue.company_id, ue.admin, c.company_name
|
|
24
|
+
FROM rapport.user_entitlements ue
|
|
25
|
+
JOIN rapport.companies c ON ue.company_id = c.company_id
|
|
26
|
+
WHERE ue.email_address = $1
|
|
27
27
|
`;
|
|
28
28
|
const entitlements = await executeQuery(entitlementQuery, [email]);
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
|
|
|
31
31
|
return createErrorResponse(403, 'No company access');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const companyIds = entitlements.rows.map(e => e.
|
|
34
|
+
const companyIds = entitlements.rows.map(e => e.company_id);
|
|
35
35
|
|
|
36
36
|
// Optional filter by specific company
|
|
37
37
|
let targetCompanyIds = companyIds;
|
|
@@ -53,7 +53,7 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
|
|
|
53
53
|
p.created_at,
|
|
54
54
|
p.last_active,
|
|
55
55
|
p.archived,
|
|
56
|
-
c.
|
|
56
|
+
c.company_name,
|
|
57
57
|
COUNT(DISTINCT pc.email_address) as collaborator_count,
|
|
58
58
|
COALESCE(
|
|
59
59
|
json_agg(
|
|
@@ -66,11 +66,11 @@ async function getProjects({ queryStringParameters: queryParams = {}, requestCon
|
|
|
66
66
|
'[]'
|
|
67
67
|
) as collaborators
|
|
68
68
|
FROM rapport.projects p
|
|
69
|
-
JOIN
|
|
69
|
+
JOIN rapport.companies c ON p.company_id = c.company_id
|
|
70
70
|
LEFT JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
|
|
71
71
|
WHERE p.company_id = ANY($1::varchar[])
|
|
72
72
|
AND p.archived = false
|
|
73
|
-
GROUP BY p.project_id, c.
|
|
73
|
+
GROUP BY p.project_id, c.company_name
|
|
74
74
|
ORDER BY p.last_active DESC NULLS LAST, p.created_at DESC
|
|
75
75
|
`;
|
|
76
76
|
|
|
@@ -30,14 +30,14 @@ async function updateProject({ body: requestBody = {}, requestContext }) {
|
|
|
30
30
|
p.project_id,
|
|
31
31
|
p.company_id,
|
|
32
32
|
pc.role,
|
|
33
|
-
ue.
|
|
33
|
+
ue.admin as company_admin
|
|
34
34
|
FROM rapport.projects p
|
|
35
35
|
LEFT JOIN rapport.project_collaborators pc
|
|
36
36
|
ON p.project_id = pc.project_id
|
|
37
37
|
AND pc.email_address = $1
|
|
38
|
-
LEFT JOIN
|
|
39
|
-
ON ue.
|
|
40
|
-
AND ue.
|
|
38
|
+
LEFT JOIN rapport.user_entitlements ue
|
|
39
|
+
ON ue.email_address = $1
|
|
40
|
+
AND ue.company_id = p.company_id
|
|
41
41
|
WHERE p.project_id = $2
|
|
42
42
|
`;
|
|
43
43
|
const accessCheck = await executeQuery(accessQuery, [email, projectId]);
|
|
@@ -20,13 +20,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
20
20
|
const period = params.period || '30d';
|
|
21
21
|
const companyId = params.Company_ID;
|
|
22
22
|
|
|
23
|
-
// Validate access - must be manager/admin
|
|
23
|
+
// Validate access - must be manager/admin
|
|
24
24
|
const accessCheck = await executeQuery(`
|
|
25
|
-
SELECT ue.
|
|
26
|
-
FROM
|
|
27
|
-
WHERE ue.
|
|
28
|
-
AND (ue.
|
|
29
|
-
${companyId ? 'AND ue.
|
|
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
30
|
LIMIT 1
|
|
31
31
|
`, companyId ? [email, companyId] : [email]);
|
|
32
32
|
|
|
@@ -34,7 +34,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
34
34
|
return createErrorResponse(403, 'Manager or Admin access required');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const userCompanyId = companyId || accessCheck.rows[0].
|
|
37
|
+
const userCompanyId = companyId || accessCheck.rows[0].company_id;
|
|
38
38
|
const periodDays = parsePeriod(period);
|
|
39
39
|
const periodStart = new Date();
|
|
40
40
|
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
@@ -49,7 +49,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
49
49
|
AVG(s.duration_seconds / 60.0) as avg_session_minutes
|
|
50
50
|
FROM rapport.sessions s
|
|
51
51
|
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
52
|
-
WHERE p.
|
|
52
|
+
WHERE p.company_id = $1
|
|
53
53
|
AND s.started_at >= $2
|
|
54
54
|
`, [userCompanyId, periodStart]);
|
|
55
55
|
} catch (e) {
|
|
@@ -69,7 +69,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
69
69
|
AVG(dm.compliance_score) as avg_compliance
|
|
70
70
|
FROM rapport.developer_metrics dm
|
|
71
71
|
JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
|
|
72
|
-
WHERE r.
|
|
72
|
+
WHERE r.company_id = $1
|
|
73
73
|
AND dm.period_start >= $2
|
|
74
74
|
GROUP BY dm.developer_email, dm.developer_name
|
|
75
75
|
ORDER BY total_commits DESC
|
|
@@ -86,7 +86,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
86
86
|
SELECT DISTINCT s.email_address
|
|
87
87
|
FROM rapport.sessions s
|
|
88
88
|
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
89
|
-
WHERE p.
|
|
89
|
+
WHERE p.company_id = $1
|
|
90
90
|
AND s.started_at >= $2
|
|
91
91
|
`, [userCompanyId, periodStart]);
|
|
92
92
|
} catch (e) {
|
|
@@ -106,6 +106,9 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
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);
|
|
109
112
|
|
|
110
113
|
return createSuccessResponse({
|
|
111
114
|
report_type: 'ai_leverage',
|
|
@@ -113,28 +116,29 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
113
116
|
period_start: periodStart.toISOString(),
|
|
114
117
|
period_end: new Date().toISOString(),
|
|
115
118
|
summary: {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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'
|
|
120
126
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
128
140
|
},
|
|
129
|
-
|
|
130
|
-
developers_count: nonAiDevs.length,
|
|
131
|
-
avg_commits: calcAvg(nonAiDevs, 'total_commits').toFixed(1),
|
|
132
|
-
avg_lines: calcAvg(nonAiDevs, 'lines_added').toFixed(0),
|
|
133
|
-
avg_prs: calcAvg(nonAiDevs, 'prs_merged').toFixed(1),
|
|
134
|
-
avg_compliance: calcAvg(nonAiDevs, 'avg_compliance').toFixed(1),
|
|
135
|
-
developers: nonAiDevs
|
|
136
|
-
},
|
|
137
|
-
productivity_multiplier: calculateMultiplier(aiAssistedDevs, nonAiDevs),
|
|
141
|
+
standards_effectiveness: [],
|
|
138
142
|
insights: generateInsights(aiAssistedDevs, nonAiDevs, sessionData)
|
|
139
143
|
});
|
|
140
144
|
});
|