@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,334 @@
1
+ /**
2
+ * Standards Compliance Scanner
3
+ * Scans committed code for standards patterns and anti-patterns
4
+ *
5
+ * Schedule: Daily after git history analysis
6
+ * Auth: None (Lambda scheduled event)
7
+ *
8
+ * Requires: GitHub token with contents:read permission
9
+ */
10
+
11
+ const { wrapHandler, executeQuery, createSuccessResponse } = require('./helpers');
12
+ const https = require('https');
13
+
14
+ exports.handler = wrapHandler(async (event, context) => {
15
+ // Check if GitHub token is available
16
+ if (!process.env.GITHUB_TOKEN) {
17
+ return createSuccessResponse({ scanned: false }, 'Compliance scan skipped - no GitHub token');
18
+ }
19
+
20
+ // Get patterns to scan for
21
+ const patterns = await getActivePatterns();
22
+
23
+ // Get recent commits that need scanning
24
+ const commits = await getUnscannedCommits();
25
+
26
+ if (commits.length === 0) {
27
+ return createSuccessResponse({ scanned: 0 }, 'No commits to scan');
28
+ }
29
+
30
+ const results = {
31
+ commits_scanned: 0,
32
+ files_scanned: 0,
33
+ patterns_found: 0,
34
+ anti_patterns_found: 0
35
+ };
36
+
37
+ for (const commit of commits) {
38
+ try {
39
+ const scanResult = await scanCommit(commit, patterns);
40
+ results.commits_scanned++;
41
+ results.files_scanned += scanResult.files_scanned;
42
+ results.patterns_found += scanResult.patterns_found;
43
+ results.anti_patterns_found += scanResult.anti_patterns_found;
44
+
45
+ // Update developer metrics with compliance data
46
+ await updateComplianceMetrics(commit, scanResult);
47
+
48
+ } catch (error) {
49
+ // Continue scanning other commits even if one fails
50
+ }
51
+ }
52
+
53
+ // Refresh team summary view
54
+ await executeQuery('SELECT rapport.refresh_team_summary()');
55
+
56
+ return createSuccessResponse(results, 'Compliance scan complete');
57
+ });
58
+
59
+ /**
60
+ * Get active patterns from database
61
+ */
62
+ async function getActivePatterns() {
63
+ const result = await executeQuery(`
64
+ SELECT pattern_id, pattern_name, pattern_type, language,
65
+ regex_pattern, severity, source
66
+ FROM rapport.code_patterns
67
+ WHERE enabled = true
68
+ `);
69
+ return result.rows;
70
+ }
71
+
72
+ /**
73
+ * Get commits that haven't been scanned yet
74
+ */
75
+ async function getUnscannedCommits() {
76
+ const result = await executeQuery(`
77
+ SELECT c.commit_sha, c.repo_id, c.author_email,
78
+ r.repo_url, r.repo_name
79
+ FROM rapport.commits c
80
+ JOIN rapport.git_repositories r ON c.repo_id = r.repo_id
81
+ WHERE c.scanned_at IS NULL
82
+ AND c.committed_at >= NOW() - INTERVAL '7 days'
83
+ ORDER BY c.committed_at DESC
84
+ LIMIT 100
85
+ `);
86
+ return result.rows;
87
+ }
88
+
89
+ /**
90
+ * Scan a single commit for patterns
91
+ */
92
+ async function scanCommit(commit, patterns) {
93
+ const result = {
94
+ files_scanned: 0,
95
+ patterns_found: 0,
96
+ anti_patterns_found: 0,
97
+ findings: []
98
+ };
99
+
100
+ // Parse owner/repo from URL
101
+ const match = commit.repo_url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
102
+ if (!match) {
103
+ console.warn(`Cannot parse GitHub URL: ${commit.repo_url}`);
104
+ return result;
105
+ }
106
+
107
+ const [, owner, repoName] = match;
108
+
109
+ // Get commit details with file changes
110
+ const commitDetails = await fetchCommitDetails(owner, repoName, commit.commit_sha);
111
+ if (!commitDetails || !commitDetails.files) {
112
+ return result;
113
+ }
114
+
115
+ for (const file of commitDetails.files) {
116
+ // Skip non-code files
117
+ if (!isCodeFile(file.filename)) continue;
118
+
119
+ // Get file content
120
+ const content = await fetchFileContent(owner, repoName, commit.commit_sha, file.filename);
121
+ if (!content) continue;
122
+
123
+ result.files_scanned++;
124
+
125
+ // Detect language from extension
126
+ const language = detectLanguage(file.filename);
127
+
128
+ // Scan for patterns
129
+ const relevantPatterns = patterns.filter(p =>
130
+ p.language === language || p.language === 'any'
131
+ );
132
+
133
+ for (const pattern of relevantPatterns) {
134
+ try {
135
+ const regex = new RegExp(pattern.regex_pattern, 'gm');
136
+ const matches = content.match(regex);
137
+
138
+ if (matches && matches.length > 0) {
139
+ if (pattern.pattern_type === 'standard') {
140
+ result.patterns_found += matches.length;
141
+ } else {
142
+ result.anti_patterns_found += matches.length;
143
+ }
144
+
145
+ result.findings.push({
146
+ file: file.filename,
147
+ pattern_name: pattern.pattern_name,
148
+ pattern_type: pattern.pattern_type,
149
+ severity: pattern.severity,
150
+ count: matches.length
151
+ });
152
+ }
153
+ } catch (regexError) {
154
+ console.warn(`Invalid regex for pattern ${pattern.pattern_name}:`, regexError.message);
155
+ }
156
+ }
157
+ }
158
+
159
+ // Mark commit as scanned
160
+ await executeQuery(`
161
+ UPDATE rapport.commits
162
+ SET scanned_at = NOW(),
163
+ compliance_data = $2
164
+ WHERE commit_sha = $1
165
+ `, [commit.commit_sha, JSON.stringify(result.findings)]);
166
+
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Fetch commit details from GitHub API
172
+ */
173
+ async function fetchCommitDetails(owner, repo, sha) {
174
+ return new Promise((resolve, reject) => {
175
+ const options = {
176
+ hostname: 'api.github.com',
177
+ path: `/repos/${owner}/${repo}/commits/${sha}`,
178
+ method: 'GET',
179
+ headers: {
180
+ 'User-Agent': 'MindMeld-ComplianceScanner/1.0',
181
+ 'Accept': 'application/vnd.github.v3+json',
182
+ 'Authorization': `token ${process.env.GITHUB_TOKEN}`
183
+ },
184
+ timeout: 30000
185
+ };
186
+
187
+ const req = https.request(options, (res) => {
188
+ let data = '';
189
+ res.on('data', chunk => data += chunk);
190
+ res.on('end', () => {
191
+ try {
192
+ if (res.statusCode !== 200) {
193
+ console.warn(`GitHub API returned ${res.statusCode} for commit ${sha}`);
194
+ resolve(null);
195
+ return;
196
+ }
197
+ resolve(JSON.parse(data));
198
+ } catch (e) {
199
+ reject(e);
200
+ }
201
+ });
202
+ });
203
+
204
+ req.on('error', reject);
205
+ req.on('timeout', () => {
206
+ req.destroy();
207
+ reject(new Error('GitHub API timeout'));
208
+ });
209
+
210
+ req.end();
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Fetch file content from GitHub API
216
+ */
217
+ async function fetchFileContent(owner, repo, sha, path) {
218
+ return new Promise((resolve, reject) => {
219
+ const options = {
220
+ hostname: 'api.github.com',
221
+ path: `/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}?ref=${sha}`,
222
+ method: 'GET',
223
+ headers: {
224
+ 'User-Agent': 'MindMeld-ComplianceScanner/1.0',
225
+ 'Accept': 'application/vnd.github.v3.raw',
226
+ 'Authorization': `token ${process.env.GITHUB_TOKEN}`
227
+ },
228
+ timeout: 30000
229
+ };
230
+
231
+ const req = https.request(options, (res) => {
232
+ let data = '';
233
+ res.on('data', chunk => data += chunk);
234
+ res.on('end', () => {
235
+ if (res.statusCode !== 200) {
236
+ resolve(null);
237
+ return;
238
+ }
239
+ resolve(data);
240
+ });
241
+ });
242
+
243
+ req.on('error', () => resolve(null));
244
+ req.on('timeout', () => {
245
+ req.destroy();
246
+ resolve(null);
247
+ });
248
+
249
+ req.end();
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Check if file is a code file worth scanning
255
+ */
256
+ function isCodeFile(filename) {
257
+ const codeExtensions = [
258
+ '.js', '.ts', '.tsx', '.jsx',
259
+ '.py', '.pyw',
260
+ '.go',
261
+ '.java',
262
+ '.rb',
263
+ '.php',
264
+ '.cs',
265
+ '.swift',
266
+ '.kt', '.kts',
267
+ '.rs',
268
+ '.c', '.cpp', '.h', '.hpp',
269
+ '.sql',
270
+ '.yaml', '.yml'
271
+ ];
272
+ return codeExtensions.some(ext => filename.endsWith(ext));
273
+ }
274
+
275
+ /**
276
+ * Detect language from file extension
277
+ */
278
+ function detectLanguage(filename) {
279
+ const ext = filename.split('.').pop().toLowerCase();
280
+ const languageMap = {
281
+ 'js': 'javascript',
282
+ 'jsx': 'javascript',
283
+ 'ts': 'javascript',
284
+ 'tsx': 'javascript',
285
+ 'py': 'python',
286
+ 'pyw': 'python',
287
+ 'go': 'go',
288
+ 'java': 'java',
289
+ 'rb': 'ruby',
290
+ 'php': 'php',
291
+ 'cs': 'csharp',
292
+ 'swift': 'swift',
293
+ 'kt': 'kotlin',
294
+ 'kts': 'kotlin',
295
+ 'rs': 'rust',
296
+ 'c': 'c',
297
+ 'cpp': 'cpp',
298
+ 'h': 'c',
299
+ 'hpp': 'cpp',
300
+ 'sql': 'sql',
301
+ 'yaml': 'yaml',
302
+ 'yml': 'yaml'
303
+ };
304
+ return languageMap[ext] || 'unknown';
305
+ }
306
+
307
+ /**
308
+ * Update developer metrics with compliance data
309
+ */
310
+ async function updateComplianceMetrics(commit, scanResult) {
311
+ const isCompliant = scanResult.anti_patterns_found === 0 && scanResult.patterns_found > 0;
312
+
313
+ await executeQuery(`
314
+ UPDATE rapport.developer_metrics
315
+ SET
316
+ standards_compliant_commits = standards_compliant_commits + $3,
317
+ anti_pattern_commits = anti_pattern_commits + $4,
318
+ compliance_score = CASE
319
+ WHEN commit_count > 0 THEN
320
+ (standards_compliant_commits + $3)::DECIMAL / commit_count * 100
321
+ ELSE 0
322
+ END
323
+ WHERE repo_id = $1
324
+ AND developer_email = $2
325
+ AND period_end >= CURRENT_DATE
326
+ `, [
327
+ commit.repo_id,
328
+ commit.author_email,
329
+ isCompliant ? 1 : 0,
330
+ scanResult.anti_patterns_found > 0 ? 1 : 0
331
+ ]);
332
+ }
333
+
334
+ // Removed manual success/failure helpers - using wrapHandler pattern
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Session End Handler
3
+ * Records session completion and outcomes
4
+ *
5
+ * POST /api/sessions/end
6
+ * Body: { session_id, project_id?, user_id?, patterns_used, patterns_learned, duration_seconds?, session_data? }
7
+ *
8
+ * Called by: pre-compact.js hook at end of session
9
+ */
10
+
11
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
12
+
13
+ /**
14
+ * Record session end and update metrics
15
+ */
16
+ async function recordSessionEnd({ body: requestBody = {}, requestContext }) {
17
+ try {
18
+ const Request_ID = requestContext?.requestId || 'unknown';
19
+
20
+ const {
21
+ session_id,
22
+ project_id,
23
+ user_id,
24
+ patterns_used = 0,
25
+ patterns_learned = 0,
26
+ duration_seconds,
27
+ working_directory,
28
+ git_branch,
29
+ session_data = {}
30
+ } = requestBody;
31
+
32
+ // Validate required fields
33
+ if (!session_id) {
34
+ return createErrorResponse(400, 'session_id is required');
35
+ }
36
+
37
+ // Try to update existing session, or create if not exists
38
+ const upsertQuery = `
39
+ INSERT INTO rapport.sessions (
40
+ session_id,
41
+ project_id,
42
+ email_address,
43
+ started_at,
44
+ ended_at,
45
+ duration_seconds,
46
+ working_directory,
47
+ git_branch,
48
+ patterns_used,
49
+ patterns_learned,
50
+ session_data
51
+ ) VALUES (
52
+ $1, $2, $3,
53
+ NOW() - INTERVAL '1 second' * COALESCE($4, 0),
54
+ NOW(),
55
+ $4, $5, $6, $7, $8, $9
56
+ )
57
+ ON CONFLICT (session_id) DO UPDATE SET
58
+ ended_at = NOW(),
59
+ duration_seconds = COALESCE(EXCLUDED.duration_seconds,
60
+ EXTRACT(EPOCH FROM (NOW() - rapport.sessions.started_at))::INTEGER),
61
+ working_directory = COALESCE(EXCLUDED.working_directory, rapport.sessions.working_directory),
62
+ git_branch = COALESCE(EXCLUDED.git_branch, rapport.sessions.git_branch),
63
+ patterns_used = EXCLUDED.patterns_used,
64
+ patterns_learned = EXCLUDED.patterns_learned,
65
+ session_data = rapport.sessions.session_data || EXCLUDED.session_data
66
+ RETURNING
67
+ session_id,
68
+ project_id,
69
+ email_address,
70
+ started_at,
71
+ ended_at,
72
+ duration_seconds,
73
+ patterns_used,
74
+ patterns_learned
75
+ `;
76
+
77
+ let result;
78
+ try {
79
+ result = await executeQuery(upsertQuery, [
80
+ session_id,
81
+ project_id || null,
82
+ user_id || 'anonymous',
83
+ duration_seconds || null,
84
+ working_directory || null,
85
+ git_branch || null,
86
+ patterns_used,
87
+ patterns_learned,
88
+ JSON.stringify(session_data)
89
+ ]);
90
+ } catch (insertError) {
91
+ // FK constraint likely failed (project_id or user doesn't exist)
92
+ console.error('[sessionEndPost] Session upsert failed:', insertError.message);
93
+
94
+ // Return success anyway - we tried
95
+ return createSuccessResponse(
96
+ {
97
+ Records: [{
98
+ session_id,
99
+ recorded: false,
100
+ error: 'Session record failed - project or user not found'
101
+ }]
102
+ },
103
+ 'Session end recorded (without DB record)',
104
+ {
105
+ Request_ID,
106
+ Timestamp: new Date().toISOString()
107
+ }
108
+ );
109
+ }
110
+
111
+ const session = result.rows[0];
112
+
113
+ // Update session_standards to mark followed (standards shown but not violated)
114
+ const updateFollowedQuery = `
115
+ UPDATE rapport.session_standards
116
+ SET followed = CASE WHEN violated IS NULL OR violated = false THEN true ELSE false END
117
+ WHERE session_id = $1 AND followed IS NULL
118
+ `;
119
+
120
+ try {
121
+ await executeQuery(updateFollowedQuery, [session_id]);
122
+ } catch (updateError) {
123
+ // Non-critical - just log
124
+ console.error('[sessionEndPost] Failed to update followed status:', updateError.message);
125
+ }
126
+
127
+ // Get summary of standards compliance for this session
128
+ const complianceQuery = `
129
+ SELECT
130
+ COUNT(*) as total_shown,
131
+ COUNT(*) FILTER (WHERE followed = true) as followed,
132
+ COUNT(*) FILTER (WHERE violated = true) as violated
133
+ FROM rapport.session_standards
134
+ WHERE session_id = $1
135
+ `;
136
+
137
+ let compliance = { total_shown: 0, followed: 0, violated: 0 };
138
+ try {
139
+ const complianceResult = await executeQuery(complianceQuery, [session_id]);
140
+ if (complianceResult.rows.length > 0) {
141
+ compliance = complianceResult.rows[0];
142
+ }
143
+ } catch (complianceError) {
144
+ // Non-critical
145
+ }
146
+
147
+ return createSuccessResponse(
148
+ {
149
+ Records: [{
150
+ session_id: session.session_id,
151
+ project_id: session.project_id,
152
+ email_address: session.email_address,
153
+ started_at: session.started_at,
154
+ ended_at: session.ended_at,
155
+ duration_seconds: session.duration_seconds,
156
+ patterns_used: session.patterns_used,
157
+ patterns_learned: session.patterns_learned,
158
+ standards_compliance: {
159
+ total_shown: parseInt(compliance.total_shown) || 0,
160
+ followed: parseInt(compliance.followed) || 0,
161
+ violated: parseInt(compliance.violated) || 0
162
+ },
163
+ recorded: true
164
+ }]
165
+ },
166
+ 'Session end recorded',
167
+ {
168
+ Total_Records: 1,
169
+ Request_ID,
170
+ Timestamp: new Date().toISOString()
171
+ }
172
+ );
173
+
174
+ } catch (error) {
175
+ console.error('Handler Error:', error);
176
+ return handleError(error);
177
+ }
178
+ }
179
+
180
+ exports.handler = wrapHandler(recordSessionEnd);
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Session Standards Handler
3
+ * Records which standards were shown during a Claude Code session
4
+ *
5
+ * POST /api/sessions/standards
6
+ * Body: { session_id, standards[], project_id?, user_id? }
7
+ *
8
+ * Called by: session-start.js hook (recordStandardsShown method)
9
+ * Fire-and-forget - should not block session start
10
+ */
11
+
12
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = require('./helpers');
13
+
14
+ /**
15
+ * Record standards shown during session
16
+ * Creates session record if needed, then records standards
17
+ */
18
+ async function recordSessionStandards({ body: requestBody = {}, requestContext }) {
19
+ try {
20
+ const Request_ID = requestContext?.requestId || 'unknown';
21
+
22
+ const {
23
+ session_id,
24
+ standards = [],
25
+ project_id,
26
+ user_id
27
+ } = requestBody;
28
+
29
+ // Validate required fields
30
+ if (!session_id) {
31
+ return createErrorResponse(400, 'session_id is required');
32
+ }
33
+
34
+ if (!standards || standards.length === 0) {
35
+ // Not an error - just nothing to record
36
+ return createSuccessResponse(
37
+ { Records: [], recorded: 0 },
38
+ 'No standards to record',
39
+ { Request_ID, Timestamp: new Date().toISOString() }
40
+ );
41
+ }
42
+
43
+ // Try to ensure session exists (upsert with minimal data)
44
+ // This may fail if project_id is not provided or invalid - that's OK
45
+ if (project_id && user_id) {
46
+ const sessionUpsertQuery = `
47
+ INSERT INTO rapport.sessions (
48
+ session_id,
49
+ project_id,
50
+ email_address,
51
+ started_at,
52
+ session_data
53
+ ) VALUES (
54
+ $1, $2, $3, NOW(), '{}'::jsonb
55
+ )
56
+ ON CONFLICT (session_id) DO NOTHING
57
+ `;
58
+
59
+ try {
60
+ await executeQuery(sessionUpsertQuery, [session_id, project_id, user_id]);
61
+ } catch (sessionError) {
62
+ // Session insert failed (FK constraint probably)
63
+ // Continue anyway - we'll try to record standards
64
+ console.error('[sessionStandardsPost] Session upsert failed:', sessionError.message);
65
+ }
66
+ }
67
+
68
+ // Record each standard shown
69
+ const recordedStandards = [];
70
+ let errors = 0;
71
+
72
+ for (const standard of standards) {
73
+ const standardId = standard.pattern_id || standard.element;
74
+ const relevanceScore = standard.relevance_score || standard.score || 0;
75
+
76
+ if (!standardId) {
77
+ errors++;
78
+ continue;
79
+ }
80
+
81
+ const insertQuery = `
82
+ INSERT INTO rapport.session_standards (
83
+ session_id,
84
+ standard_id,
85
+ relevance_score,
86
+ shown_at
87
+ ) VALUES (
88
+ $1, $2, $3, NOW()
89
+ )
90
+ ON CONFLICT (session_id, standard_id) DO UPDATE SET
91
+ relevance_score = EXCLUDED.relevance_score,
92
+ shown_at = NOW()
93
+ RETURNING id
94
+ `;
95
+
96
+ try {
97
+ const result = await executeQuery(insertQuery, [
98
+ session_id,
99
+ standardId,
100
+ relevanceScore
101
+ ]);
102
+
103
+ recordedStandards.push({
104
+ id: result.rows[0]?.id,
105
+ standard_id: standardId,
106
+ relevance_score: relevanceScore
107
+ });
108
+ } catch (insertError) {
109
+ // Standard might not exist or FK constraint failed
110
+ console.error(`[sessionStandardsPost] Failed to record standard ${standardId}:`, insertError.message);
111
+ errors++;
112
+ }
113
+ }
114
+
115
+ return createSuccessResponse(
116
+ {
117
+ Records: recordedStandards,
118
+ recorded: recordedStandards.length,
119
+ errors: errors
120
+ },
121
+ `Recorded ${recordedStandards.length} standard(s)`,
122
+ {
123
+ Total_Records: recordedStandards.length,
124
+ Request_ID,
125
+ Timestamp: new Date().toISOString()
126
+ }
127
+ );
128
+
129
+ } catch (error) {
130
+ console.error('Handler Error:', error);
131
+ return handleError(error);
132
+ }
133
+ }
134
+
135
+ exports.handler = wrapHandler(recordSessionStandards);