@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.
Files changed (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. 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);