@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,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);
|