@equilateral_ai/mindmeld 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get Project Correlations Handler
|
|
3
|
+
* Returns session-to-commit correlation data for a specific project
|
|
4
|
+
*
|
|
5
|
+
* GET /api/correlations/project/{projectId}
|
|
6
|
+
* Query params:
|
|
7
|
+
* - lookbackDays (optional, default: 30)
|
|
8
|
+
*
|
|
9
|
+
* Returns:
|
|
10
|
+
* - Project productivity metrics
|
|
11
|
+
* - Developer breakdown
|
|
12
|
+
* - Pattern effectiveness for project
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
16
|
+
const { CorrelationAnalyzer } = require('../../core/CorrelationAnalyzer');
|
|
17
|
+
|
|
18
|
+
exports.handler = wrapHandler(async (event, context) => {
|
|
19
|
+
// Extract user email from Cognito claims
|
|
20
|
+
const email = event.requestContext?.authorizer?.claims?.email;
|
|
21
|
+
if (!email) {
|
|
22
|
+
return createErrorResponse(401, 'Unauthorized - no email in claims');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get project ID from path parameters
|
|
26
|
+
const projectId = event.pathParameters?.projectId;
|
|
27
|
+
if (!projectId) {
|
|
28
|
+
return createErrorResponse(400, 'Project ID is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse query parameters
|
|
32
|
+
const queryParams = event.queryStringParameters || {};
|
|
33
|
+
const lookbackDays = parseInt(queryParams.lookbackDays) || 30;
|
|
34
|
+
|
|
35
|
+
// Verify user has access to this project
|
|
36
|
+
const accessResult = await executeQuery(`
|
|
37
|
+
SELECT p.project_id, p.project_name, p.company_id
|
|
38
|
+
FROM rapport.projects p
|
|
39
|
+
JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
|
|
40
|
+
WHERE p.project_id = $1
|
|
41
|
+
AND pc.email_address = $2
|
|
42
|
+
`, [projectId, email]);
|
|
43
|
+
|
|
44
|
+
if (accessResult.rows.length === 0) {
|
|
45
|
+
return createErrorResponse(403, 'Access denied to this project');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const project = accessResult.rows[0];
|
|
49
|
+
|
|
50
|
+
// Initialize analyzer
|
|
51
|
+
const analyzer = new CorrelationAnalyzer();
|
|
52
|
+
|
|
53
|
+
// Get project productivity metrics
|
|
54
|
+
const projectMetrics = await analyzer.getProjectProductivity(projectId, lookbackDays);
|
|
55
|
+
|
|
56
|
+
// Get developer breakdown for the project
|
|
57
|
+
const developerBreakdown = await getDeveloperBreakdown(projectId, lookbackDays);
|
|
58
|
+
|
|
59
|
+
// Get pattern effectiveness for this project
|
|
60
|
+
const patternEffectiveness = await analyzer.getPatternEffectiveness(projectId, lookbackDays);
|
|
61
|
+
|
|
62
|
+
// Get recent correlations
|
|
63
|
+
const recentCorrelations = await getRecentCorrelations(projectId, 10);
|
|
64
|
+
|
|
65
|
+
return createSuccessResponse({
|
|
66
|
+
project: {
|
|
67
|
+
projectId: project.project_id,
|
|
68
|
+
projectName: project.project_name,
|
|
69
|
+
companyId: project.company_id
|
|
70
|
+
},
|
|
71
|
+
metrics: projectMetrics,
|
|
72
|
+
developerBreakdown,
|
|
73
|
+
patternEffectiveness,
|
|
74
|
+
recentCorrelations
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get developer breakdown for a project
|
|
80
|
+
*/
|
|
81
|
+
async function getDeveloperBreakdown(projectId, lookbackDays) {
|
|
82
|
+
const query = `
|
|
83
|
+
SELECT
|
|
84
|
+
sc.email_address,
|
|
85
|
+
u."User_Display_Name" as display_name,
|
|
86
|
+
COUNT(*) as total_sessions,
|
|
87
|
+
COUNT(*) FILTER (WHERE sc.has_commits = true) as productive_sessions,
|
|
88
|
+
ROUND(
|
|
89
|
+
COUNT(*) FILTER (WHERE sc.has_commits = true)::decimal /
|
|
90
|
+
NULLIF(COUNT(*), 0) * 100,
|
|
91
|
+
1
|
|
92
|
+
) as conversion_rate,
|
|
93
|
+
SUM(sc.commit_count) as total_commits,
|
|
94
|
+
SUM(sc.total_insertions) as total_insertions,
|
|
95
|
+
SUM(sc.total_deletions) as total_deletions,
|
|
96
|
+
ROUND(AVG(sc.session_duration_seconds) / 60, 0) as avg_session_minutes,
|
|
97
|
+
MAX(sc.session_started_at) as last_session
|
|
98
|
+
FROM rapport.session_correlations sc
|
|
99
|
+
JOIN "Users" u ON sc.email_address = u."Email_Address"
|
|
100
|
+
WHERE sc.project_id = $1
|
|
101
|
+
AND sc.session_started_at > NOW() - INTERVAL '${lookbackDays} days'
|
|
102
|
+
GROUP BY sc.email_address, u."User_Display_Name"
|
|
103
|
+
ORDER BY total_commits DESC NULLS LAST
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const result = await executeQuery(query, [projectId]);
|
|
107
|
+
|
|
108
|
+
return result.rows.map(row => ({
|
|
109
|
+
email: row.email_address,
|
|
110
|
+
displayName: row.display_name,
|
|
111
|
+
totalSessions: parseInt(row.total_sessions) || 0,
|
|
112
|
+
productiveSessions: parseInt(row.productive_sessions) || 0,
|
|
113
|
+
conversionRate: parseFloat(row.conversion_rate) || 0,
|
|
114
|
+
totalCommits: parseInt(row.total_commits) || 0,
|
|
115
|
+
totalInsertions: parseInt(row.total_insertions) || 0,
|
|
116
|
+
totalDeletions: parseInt(row.total_deletions) || 0,
|
|
117
|
+
avgSessionMinutes: parseInt(row.avg_session_minutes) || 0,
|
|
118
|
+
lastSession: row.last_session
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get recent correlations for a project
|
|
124
|
+
*/
|
|
125
|
+
async function getRecentCorrelations(projectId, limit) {
|
|
126
|
+
const query = `
|
|
127
|
+
SELECT
|
|
128
|
+
sc.session_id,
|
|
129
|
+
sc.email_address,
|
|
130
|
+
u."User_Display_Name" as display_name,
|
|
131
|
+
sc.session_started_at,
|
|
132
|
+
sc.session_duration_seconds,
|
|
133
|
+
sc.has_commits,
|
|
134
|
+
sc.commit_count,
|
|
135
|
+
sc.total_insertions,
|
|
136
|
+
sc.total_deletions,
|
|
137
|
+
sc.correlation_type,
|
|
138
|
+
sc.correlation_score
|
|
139
|
+
FROM rapport.session_correlations sc
|
|
140
|
+
JOIN "Users" u ON sc.email_address = u."Email_Address"
|
|
141
|
+
WHERE sc.project_id = $1
|
|
142
|
+
ORDER BY sc.session_started_at DESC
|
|
143
|
+
LIMIT $2
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const result = await executeQuery(query, [projectId, limit]);
|
|
147
|
+
|
|
148
|
+
return result.rows.map(row => ({
|
|
149
|
+
sessionId: row.session_id,
|
|
150
|
+
email: row.email_address,
|
|
151
|
+
displayName: row.display_name,
|
|
152
|
+
sessionStarted: row.session_started_at,
|
|
153
|
+
sessionDurationMinutes: Math.round((row.session_duration_seconds || 0) / 60),
|
|
154
|
+
hasCommits: row.has_commits,
|
|
155
|
+
commitCount: row.commit_count,
|
|
156
|
+
totalInsertions: row.total_insertions,
|
|
157
|
+
totalDeletions: row.total_deletions,
|
|
158
|
+
correlationType: row.correlation_type,
|
|
159
|
+
correlationScore: parseFloat(row.correlation_score) || 0
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Connection Status Handler
|
|
3
|
+
* Returns current GitHub connection status
|
|
4
|
+
*
|
|
5
|
+
* GET /api/github/status (CognitoAuthorizer)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('../helpers');
|
|
9
|
+
|
|
10
|
+
async function githubConnectionStatus({ requestContext }) {
|
|
11
|
+
try {
|
|
12
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
13
|
+
|
|
14
|
+
if (!email) {
|
|
15
|
+
return createErrorResponse(401, 'Authentication required');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for active connection
|
|
19
|
+
const connResult = await executeQuery(`
|
|
20
|
+
SELECT github_username, connected_at, last_used_at
|
|
21
|
+
FROM rapport.github_connections
|
|
22
|
+
WHERE email_address = $1 AND revoked = FALSE
|
|
23
|
+
`, [email]);
|
|
24
|
+
|
|
25
|
+
const connected = connResult.rowCount > 0;
|
|
26
|
+
const github_username = connected ? connResult.rows[0].github_username : null;
|
|
27
|
+
|
|
28
|
+
// Count projects with GitHub connected
|
|
29
|
+
const projectsResult = await executeQuery(`
|
|
30
|
+
SELECT COUNT(*) as count FROM rapport.projects p
|
|
31
|
+
JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
|
|
32
|
+
WHERE pc.email_address = $1
|
|
33
|
+
AND p.github_owner IS NOT NULL
|
|
34
|
+
AND p.archived = FALSE
|
|
35
|
+
`, [email]);
|
|
36
|
+
|
|
37
|
+
const projects_connected = parseInt(projectsResult.rows[0].count);
|
|
38
|
+
|
|
39
|
+
return createSuccessResponse(
|
|
40
|
+
{ connected, github_username, projects_connected },
|
|
41
|
+
'Status retrieved'
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('GitHub Status Error:', error);
|
|
45
|
+
return createErrorResponse(500, 'Failed to get connection status');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exports.handler = wrapHandler(githubConnectionStatus);
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Discover Patterns Handler
|
|
3
|
+
* Analyzes a GitHub repo to discover coding patterns
|
|
4
|
+
*
|
|
5
|
+
* POST /api/github/discover (CognitoAuthorizer)
|
|
6
|
+
* Body: { project_id, owner, repo, branch }
|
|
7
|
+
* Timeout: 120s, Memory: 512MB
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('../helpers');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const https = require('https');
|
|
13
|
+
|
|
14
|
+
function httpsGet(path, token) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const req = https.request({
|
|
17
|
+
hostname: 'api.github.com',
|
|
18
|
+
path: path,
|
|
19
|
+
method: 'GET',
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${token}`,
|
|
22
|
+
'User-Agent': 'MindMeld-App',
|
|
23
|
+
'Accept': 'application/json'
|
|
24
|
+
}
|
|
25
|
+
}, (res) => {
|
|
26
|
+
let data = '';
|
|
27
|
+
res.on('data', chunk => data += chunk);
|
|
28
|
+
res.on('end', () => {
|
|
29
|
+
try {
|
|
30
|
+
resolve({ statusCode: res.statusCode, body: JSON.parse(data) });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
resolve({ statusCode: res.statusCode, body: data });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
req.on('error', reject);
|
|
37
|
+
req.end();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decryptToken(encryptedData, key) {
|
|
42
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
43
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
44
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
|
45
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
46
|
+
decrypted += decipher.final('utf8');
|
|
47
|
+
return decrypted;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function githubDiscoverPatterns({ body, requestContext }) {
|
|
51
|
+
try {
|
|
52
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
53
|
+
|
|
54
|
+
if (!email) {
|
|
55
|
+
return createErrorResponse(401, 'Authentication required');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { project_id, owner, repo, branch } = body;
|
|
59
|
+
if (!project_id || !owner || !repo) {
|
|
60
|
+
return createErrorResponse(400, 'project_id, owner, and repo are required');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const targetBranch = branch || 'main';
|
|
64
|
+
|
|
65
|
+
// Verify user has access to project
|
|
66
|
+
const accessResult = await executeQuery(`
|
|
67
|
+
SELECT role FROM rapport.project_collaborators
|
|
68
|
+
WHERE project_id = $1 AND email_address = $2
|
|
69
|
+
`, [project_id, email]);
|
|
70
|
+
|
|
71
|
+
if (accessResult.rowCount === 0) {
|
|
72
|
+
return createErrorResponse(403, 'Access denied to project');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get decrypted token
|
|
76
|
+
const connResult = await executeQuery(`
|
|
77
|
+
SELECT access_token_encrypted FROM rapport.github_connections
|
|
78
|
+
WHERE email_address = $1 AND revoked = FALSE
|
|
79
|
+
`, [email]);
|
|
80
|
+
|
|
81
|
+
if (connResult.rowCount === 0) {
|
|
82
|
+
return createErrorResponse(404, 'No GitHub connection found');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const encryptionKey = process.env.GITHUB_TOKEN_ENCRYPTION_KEY;
|
|
86
|
+
const accessToken = decryptToken(connResult.rows[0].access_token_encrypted, encryptionKey);
|
|
87
|
+
|
|
88
|
+
// Fetch repo data in parallel
|
|
89
|
+
const [languagesRes, treeRes, commitsRes] = await Promise.all([
|
|
90
|
+
httpsGet(`/repos/${owner}/${repo}/languages`, accessToken),
|
|
91
|
+
httpsGet(`/repos/${owner}/${repo}/git/trees/${targetBranch}?recursive=1`, accessToken),
|
|
92
|
+
httpsGet(`/repos/${owner}/${repo}/commits?per_page=50`, accessToken)
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const discoveries = [];
|
|
96
|
+
|
|
97
|
+
// --- Analyze Languages (tech_stack) ---
|
|
98
|
+
if (languagesRes.statusCode === 200 && languagesRes.body) {
|
|
99
|
+
const languages = languagesRes.body;
|
|
100
|
+
const totalBytes = Object.values(languages).reduce((a, b) => a + b, 0);
|
|
101
|
+
|
|
102
|
+
for (const [lang, bytes] of Object.entries(languages)) {
|
|
103
|
+
const percentage = bytes / totalBytes;
|
|
104
|
+
if (percentage >= 0.05) { // At least 5% of codebase
|
|
105
|
+
discoveries.push({
|
|
106
|
+
discovery_type: 'tech_stack',
|
|
107
|
+
pattern_name: `Primary language: ${lang}`,
|
|
108
|
+
pattern_description: `${lang} makes up ${Math.round(percentage * 100)}% of the codebase`,
|
|
109
|
+
confidence: Math.min(percentage * 2, 0.99),
|
|
110
|
+
evidence: [{ source: 'languages_api', percentage: Math.round(percentage * 100) }]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Analyze File Tree (architecture, testing, ci_cd, naming) ---
|
|
117
|
+
if (treeRes.statusCode === 200 && treeRes.body?.tree) {
|
|
118
|
+
const tree = treeRes.body.tree;
|
|
119
|
+
const files = tree.filter(t => t.type === 'blob').map(t => t.path);
|
|
120
|
+
const dirs = tree.filter(t => t.type === 'tree').map(t => t.path);
|
|
121
|
+
|
|
122
|
+
// Detect test framework
|
|
123
|
+
const testFiles = files.filter(f =>
|
|
124
|
+
f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__/')
|
|
125
|
+
);
|
|
126
|
+
if (testFiles.length > 0) {
|
|
127
|
+
const isJest = testFiles.some(f => f.endsWith('.test.js') || f.endsWith('.test.ts'));
|
|
128
|
+
const isMocha = files.some(f => f.includes('.mocharc') || f.includes('mocha'));
|
|
129
|
+
const isPytest = testFiles.some(f => f.startsWith('test_') || f.includes('/test_'));
|
|
130
|
+
|
|
131
|
+
let framework = 'Unknown';
|
|
132
|
+
if (isJest) framework = 'Jest';
|
|
133
|
+
else if (isMocha) framework = 'Mocha';
|
|
134
|
+
else if (isPytest) framework = 'pytest';
|
|
135
|
+
|
|
136
|
+
discoveries.push({
|
|
137
|
+
discovery_type: 'testing',
|
|
138
|
+
pattern_name: `Test framework: ${framework}`,
|
|
139
|
+
pattern_description: `Found ${testFiles.length} test files using ${framework} conventions`,
|
|
140
|
+
confidence: Math.min(testFiles.length / 10, 0.95),
|
|
141
|
+
evidence: [{ source: 'file_tree', test_files_count: testFiles.length, sample: testFiles.slice(0, 5) }]
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Detect CI/CD
|
|
146
|
+
const ciFiles = files.filter(f =>
|
|
147
|
+
f.startsWith('.github/workflows/') || f === '.gitlab-ci.yml' ||
|
|
148
|
+
f === 'Jenkinsfile' || f === '.circleci/config.yml'
|
|
149
|
+
);
|
|
150
|
+
if (ciFiles.length > 0) {
|
|
151
|
+
const ciTool = ciFiles[0].startsWith('.github/') ? 'GitHub Actions' :
|
|
152
|
+
ciFiles[0].includes('gitlab') ? 'GitLab CI' :
|
|
153
|
+
ciFiles[0] === 'Jenkinsfile' ? 'Jenkins' : 'CircleCI';
|
|
154
|
+
|
|
155
|
+
discoveries.push({
|
|
156
|
+
discovery_type: 'ci_cd',
|
|
157
|
+
pattern_name: `CI/CD: ${ciTool}`,
|
|
158
|
+
pattern_description: `Uses ${ciTool} with ${ciFiles.length} workflow file(s)`,
|
|
159
|
+
confidence: 0.95,
|
|
160
|
+
evidence: [{ source: 'file_tree', ci_files: ciFiles }]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Detect Docker
|
|
165
|
+
const dockerFiles = files.filter(f =>
|
|
166
|
+
f === 'Dockerfile' || f === 'docker-compose.yml' || f === 'docker-compose.yaml' ||
|
|
167
|
+
f.endsWith('/Dockerfile')
|
|
168
|
+
);
|
|
169
|
+
if (dockerFiles.length > 0) {
|
|
170
|
+
discoveries.push({
|
|
171
|
+
discovery_type: 'ci_cd',
|
|
172
|
+
pattern_name: 'Containerization: Docker',
|
|
173
|
+
pattern_description: `Found ${dockerFiles.length} Docker configuration file(s)`,
|
|
174
|
+
confidence: 0.9,
|
|
175
|
+
evidence: [{ source: 'file_tree', docker_files: dockerFiles }]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Detect architecture patterns
|
|
180
|
+
const srcDirs = dirs.filter(d => d.split('/').length <= 2);
|
|
181
|
+
const hasSrcDir = srcDirs.some(d => d === 'src' || d.startsWith('src/'));
|
|
182
|
+
const hasLibDir = srcDirs.some(d => d === 'lib' || d.startsWith('lib/'));
|
|
183
|
+
const hasComponentsDir = dirs.some(d => d.includes('components'));
|
|
184
|
+
const hasPagesDir = dirs.some(d => d.includes('pages') || d.includes('views'));
|
|
185
|
+
const hasHandlersDir = dirs.some(d => d.includes('handlers') || d.includes('controllers'));
|
|
186
|
+
|
|
187
|
+
if (hasComponentsDir && hasPagesDir) {
|
|
188
|
+
discoveries.push({
|
|
189
|
+
discovery_type: 'architecture',
|
|
190
|
+
pattern_name: 'Frontend: Component-based architecture',
|
|
191
|
+
pattern_description: 'Uses components/ and pages/ directory structure (React/Vue/Svelte pattern)',
|
|
192
|
+
confidence: 0.85,
|
|
193
|
+
evidence: [{ source: 'file_tree', pattern: 'components_pages' }]
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (hasHandlersDir) {
|
|
198
|
+
discoveries.push({
|
|
199
|
+
discovery_type: 'architecture',
|
|
200
|
+
pattern_name: 'Backend: Handler/Controller pattern',
|
|
201
|
+
pattern_description: 'Uses handlers/ or controllers/ for request handling',
|
|
202
|
+
confidence: 0.8,
|
|
203
|
+
evidence: [{ source: 'file_tree', pattern: 'handlers_controllers' }]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Detect naming conventions
|
|
208
|
+
const jsFiles = files.filter(f => f.endsWith('.js') || f.endsWith('.ts'));
|
|
209
|
+
const camelCaseFiles = jsFiles.filter(f => {
|
|
210
|
+
const name = f.split('/').pop().replace(/\.(js|ts|tsx|jsx)$/, '');
|
|
211
|
+
return /^[a-z][a-zA-Z]*$/.test(name);
|
|
212
|
+
});
|
|
213
|
+
const kebabCaseFiles = jsFiles.filter(f => {
|
|
214
|
+
const name = f.split('/').pop().replace(/\.(js|ts|tsx|jsx)$/, '');
|
|
215
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(name);
|
|
216
|
+
});
|
|
217
|
+
const pascalCaseFiles = jsFiles.filter(f => {
|
|
218
|
+
const name = f.split('/').pop().replace(/\.(js|ts|tsx|jsx)$/, '');
|
|
219
|
+
return /^[A-Z][a-zA-Z]*$/.test(name);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (jsFiles.length > 5) {
|
|
223
|
+
let namingConvention = 'mixed';
|
|
224
|
+
let confidence = 0.5;
|
|
225
|
+
const total = jsFiles.length;
|
|
226
|
+
|
|
227
|
+
if (camelCaseFiles.length / total > 0.6) {
|
|
228
|
+
namingConvention = 'camelCase';
|
|
229
|
+
confidence = camelCaseFiles.length / total;
|
|
230
|
+
} else if (kebabCaseFiles.length / total > 0.6) {
|
|
231
|
+
namingConvention = 'kebab-case';
|
|
232
|
+
confidence = kebabCaseFiles.length / total;
|
|
233
|
+
} else if (pascalCaseFiles.length / total > 0.6) {
|
|
234
|
+
namingConvention = 'PascalCase';
|
|
235
|
+
confidence = pascalCaseFiles.length / total;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (namingConvention !== 'mixed') {
|
|
239
|
+
discoveries.push({
|
|
240
|
+
discovery_type: 'naming',
|
|
241
|
+
pattern_name: `File naming: ${namingConvention}`,
|
|
242
|
+
pattern_description: `${Math.round(confidence * 100)}% of JS/TS files use ${namingConvention} naming`,
|
|
243
|
+
confidence: Math.min(confidence, 0.95),
|
|
244
|
+
evidence: [{ source: 'file_tree', convention: namingConvention, sample_count: jsFiles.length }]
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Detect TypeScript
|
|
250
|
+
const tsFiles = files.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
251
|
+
const tsconfigExists = files.some(f => f === 'tsconfig.json');
|
|
252
|
+
if (tsFiles.length > 0 && tsconfigExists) {
|
|
253
|
+
discoveries.push({
|
|
254
|
+
discovery_type: 'tech_stack',
|
|
255
|
+
pattern_name: 'TypeScript enabled',
|
|
256
|
+
pattern_description: `Found ${tsFiles.length} TypeScript files with tsconfig.json`,
|
|
257
|
+
confidence: 0.95,
|
|
258
|
+
evidence: [{ source: 'file_tree', ts_files_count: tsFiles.length }]
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- Analyze package.json if available ---
|
|
264
|
+
const pkgRes = await httpsGet(`/repos/${owner}/${repo}/contents/package.json`, accessToken);
|
|
265
|
+
if (pkgRes.statusCode === 200 && pkgRes.body?.content) {
|
|
266
|
+
try {
|
|
267
|
+
const pkgContent = JSON.parse(Buffer.from(pkgRes.body.content, 'base64').toString());
|
|
268
|
+
|
|
269
|
+
// Detect frameworks from dependencies
|
|
270
|
+
const allDeps = { ...pkgContent.dependencies, ...pkgContent.devDependencies };
|
|
271
|
+
const frameworks = [];
|
|
272
|
+
|
|
273
|
+
if (allDeps['react']) frameworks.push('React');
|
|
274
|
+
if (allDeps['next']) frameworks.push('Next.js');
|
|
275
|
+
if (allDeps['vue']) frameworks.push('Vue.js');
|
|
276
|
+
if (allDeps['express']) frameworks.push('Express');
|
|
277
|
+
if (allDeps['fastify']) frameworks.push('Fastify');
|
|
278
|
+
if (allDeps['@angular/core']) frameworks.push('Angular');
|
|
279
|
+
if (allDeps['svelte']) frameworks.push('Svelte');
|
|
280
|
+
if (allDeps['tailwindcss']) frameworks.push('Tailwind CSS');
|
|
281
|
+
|
|
282
|
+
for (const fw of frameworks) {
|
|
283
|
+
discoveries.push({
|
|
284
|
+
discovery_type: 'tech_stack',
|
|
285
|
+
pattern_name: `Framework: ${fw}`,
|
|
286
|
+
pattern_description: `${fw} is listed as a dependency`,
|
|
287
|
+
confidence: 0.95,
|
|
288
|
+
evidence: [{ source: 'package_json', dependency: fw.toLowerCase() }]
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Detect test runner from devDeps
|
|
293
|
+
if (allDeps['jest'] || allDeps['@jest/core']) {
|
|
294
|
+
// Only add if not already detected from file tree
|
|
295
|
+
if (!discoveries.some(d => d.pattern_name.includes('Jest'))) {
|
|
296
|
+
discoveries.push({
|
|
297
|
+
discovery_type: 'testing',
|
|
298
|
+
pattern_name: 'Test framework: Jest',
|
|
299
|
+
pattern_description: 'Jest is configured as a dev dependency',
|
|
300
|
+
confidence: 0.9,
|
|
301
|
+
evidence: [{ source: 'package_json' }]
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (e) {
|
|
306
|
+
console.log('Failed to parse package.json:', e.message);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Analyze Commit Patterns ---
|
|
311
|
+
if (commitsRes.statusCode === 200 && Array.isArray(commitsRes.body)) {
|
|
312
|
+
const commits = commitsRes.body;
|
|
313
|
+
|
|
314
|
+
// Detect conventional commits
|
|
315
|
+
const conventionalPattern = /^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\(.+\))?: /;
|
|
316
|
+
const conventionalCount = commits.filter(c =>
|
|
317
|
+
conventionalPattern.test(c.commit?.message || '')
|
|
318
|
+
).length;
|
|
319
|
+
|
|
320
|
+
if (conventionalCount > commits.length * 0.3) {
|
|
321
|
+
discoveries.push({
|
|
322
|
+
discovery_type: 'naming',
|
|
323
|
+
pattern_name: 'Commit convention: Conventional Commits',
|
|
324
|
+
pattern_description: `${Math.round(conventionalCount / commits.length * 100)}% of recent commits follow conventional commit format`,
|
|
325
|
+
confidence: Math.min(conventionalCount / commits.length, 0.95),
|
|
326
|
+
evidence: [{ source: 'commits', conventional_count: conventionalCount, total: commits.length }]
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Filter out low-confidence discoveries
|
|
332
|
+
const validDiscoveries = discoveries.filter(d => d.confidence >= 0.3);
|
|
333
|
+
|
|
334
|
+
// Update project with GitHub info
|
|
335
|
+
await executeQuery(`
|
|
336
|
+
UPDATE rapport.projects SET github_owner = $1, github_repo = $2
|
|
337
|
+
WHERE project_id = $3
|
|
338
|
+
`, [owner, repo, project_id]);
|
|
339
|
+
|
|
340
|
+
// Save discoveries to DB
|
|
341
|
+
for (const discovery of validDiscoveries) {
|
|
342
|
+
await executeQuery(`
|
|
343
|
+
INSERT INTO rapport.onboarding_discoveries (
|
|
344
|
+
project_id, email_address, discovery_type, pattern_name,
|
|
345
|
+
pattern_description, confidence, evidence
|
|
346
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
347
|
+
`, [
|
|
348
|
+
project_id, email, discovery.discovery_type, discovery.pattern_name,
|
|
349
|
+
discovery.pattern_description, discovery.confidence,
|
|
350
|
+
JSON.stringify(discovery.evidence)
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return createSuccessResponse(
|
|
355
|
+
{ discoveries: validDiscoveries, count: validDiscoveries.length },
|
|
356
|
+
'Pattern discovery complete'
|
|
357
|
+
);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('GitHub Discover Patterns Error:', error);
|
|
360
|
+
return createErrorResponse(500, 'Failed to discover patterns');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
exports.handler = wrapHandler(githubDiscoverPatterns);
|