@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
package/src/index.js ADDED
@@ -0,0 +1,969 @@
1
+ /**
2
+ * MindMeld - mindmeld.dev
3
+ *
4
+ * Intelligent standards injection for AI coding sessions.
5
+ */
6
+
7
+ const TeamLoadBearingDetector = require('./core/TeamLoadBearingDetector');
8
+ const CollaborationPrompt = require('./collaboration/CollaborationPrompt');
9
+ const { RelevanceDetector } = require('./core/RelevanceDetector');
10
+ const { PatternValidator } = require('./core/PatternValidator');
11
+ const { StandardsIngestion } = require('./core/StandardsIngestion');
12
+
13
+ class MindmeldClient {
14
+ constructor(config = {}) {
15
+ this.config = {
16
+ projectPath: config.projectPath || process.cwd(),
17
+ userId: config.userId || process.env.USER || 'unknown',
18
+ apiUrl: config.apiUrl || process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev',
19
+ authToken: config.authToken || process.env.MINDMELD_AUTH_TOKEN || null,
20
+ companyId: config.companyId || process.env.MINDMELD_COMPANY_ID || null,
21
+ standardsPath: config.standardsPath || null,
22
+ ...config
23
+ };
24
+
25
+ // Initialize components
26
+ this.teamDetector = new TeamLoadBearingDetector();
27
+ this.collaborationPrompt = new CollaborationPrompt();
28
+
29
+ // Phase 2-5 components (Claude Code integration)
30
+ this.relevanceDetector = new RelevanceDetector({
31
+ workingDirectory: this.config.projectPath,
32
+ standardsPath: this.config.standardsPath
33
+ });
34
+ this.patternValidator = new PatternValidator();
35
+ this.standardsIngestion = new StandardsIngestion({
36
+ standardsPath: this.config.standardsPath
37
+ });
38
+
39
+ // Project context
40
+ this.currentProject = null;
41
+ }
42
+
43
+ /**
44
+ * Detect and initialize project
45
+ */
46
+ async detectProject() {
47
+ const projectName = this.getProjectName();
48
+
49
+ // Check if project already registered
50
+ const existing = await this.getProject(projectName);
51
+
52
+ if (existing) {
53
+ this.currentProject = existing;
54
+ return existing;
55
+ }
56
+
57
+ // New project - prompt for collaboration
58
+ const result = await this.collaborationPrompt.prompt(
59
+ this.config.projectPath,
60
+ projectName
61
+ );
62
+
63
+ if (result.collaborate) {
64
+ // Initialize collaborative project
65
+ this.currentProject = await this.initializeProject(projectName, {
66
+ collaborators: result.collaborators,
67
+ private: result.private
68
+ });
69
+ } else {
70
+ // Initialize solo project
71
+ this.currentProject = await this.initializeProject(projectName, {
72
+ collaborators: [],
73
+ private: true
74
+ });
75
+ }
76
+
77
+ return this.currentProject;
78
+ }
79
+
80
+ /**
81
+ * Get project name from directory
82
+ */
83
+ getProjectName() {
84
+ const path = require('path');
85
+ return path.basename(this.config.projectPath);
86
+ }
87
+
88
+ /**
89
+ * Get existing project by name or ID
90
+ * Fetches project from API if authenticated, falls back to local storage
91
+ * @param {string} projectNameOrId - Project name or project ID
92
+ * @returns {Promise<Object|null>} Project object or null if not found
93
+ */
94
+ async getProject(projectNameOrId) {
95
+ // Try API first if authenticated
96
+ if (this.config.authToken) {
97
+ try {
98
+ const apiUrl = this.config.apiUrl;
99
+ const companyId = this.config.companyId;
100
+
101
+ // Build query params
102
+ const params = new URLSearchParams();
103
+ if (companyId) {
104
+ params.append('Company_ID', companyId);
105
+ }
106
+
107
+ const response = await this._makeApiRequest(
108
+ `${apiUrl}/api/projects?${params.toString()}`,
109
+ 'GET'
110
+ );
111
+
112
+ if (response.error) {
113
+ console.error('[Rapport] getProject API error:', response.error);
114
+ // Fall through to local storage
115
+ } else if (response.Records) {
116
+ // Search for project by name or ID
117
+ const project = response.Records.find(
118
+ p => p.project_name === projectNameOrId || p.project_id === projectNameOrId
119
+ );
120
+
121
+ if (project) {
122
+ return {
123
+ projectId: project.project_id,
124
+ projectName: project.project_name,
125
+ companyId: project.company_id,
126
+ description: project.description,
127
+ private: project.private,
128
+ collaborators: project.collaborators || [],
129
+ createdAt: project.created_at,
130
+ lastActive: project.last_active
131
+ };
132
+ }
133
+ }
134
+ } catch (error) {
135
+ console.error('[Rapport] getProject API call failed:', error.message);
136
+ // Fall through to local storage
137
+ }
138
+ }
139
+
140
+ // Fallback: check local storage
141
+ const fs = require('fs').promises;
142
+ const path = require('path');
143
+
144
+ try {
145
+ // Try to find project in local projects directory
146
+ const projectsDir = path.join(__dirname, '../projects');
147
+ const dirs = await fs.readdir(projectsDir).catch(() => []);
148
+
149
+ for (const dir of dirs) {
150
+ const configPath = path.join(projectsDir, dir, 'config.json');
151
+ try {
152
+ const content = await fs.readFile(configPath, 'utf-8');
153
+ const project = JSON.parse(content);
154
+ if (project.projectName === projectNameOrId || project.projectId === projectNameOrId) {
155
+ return project;
156
+ }
157
+ } catch (e) {
158
+ // Skip invalid config files
159
+ }
160
+ }
161
+ } catch (error) {
162
+ // Local storage not available
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ /**
169
+ * Initialize new project
170
+ * Creates project via API if authenticated, stores locally as fallback
171
+ * @param {string} projectName - Name of the project
172
+ * @param {Object} options - Project options
173
+ * @param {Array} options.collaborators - List of collaborator emails
174
+ * @param {boolean} options.private - Whether project is private
175
+ * @param {string} options.description - Project description
176
+ * @param {string} options.repoUrl - Repository URL
177
+ * @returns {Promise<Object>} Created project object
178
+ */
179
+ async initializeProject(projectName, options = {}) {
180
+ const project = this.collaborationPrompt.createProjectConfig(
181
+ projectName,
182
+ options.collaborators || [],
183
+ {
184
+ private: options.private || false
185
+ }
186
+ );
187
+
188
+ // Try API first if authenticated
189
+ if (this.config.authToken && this.config.companyId) {
190
+ try {
191
+ const apiUrl = this.config.apiUrl;
192
+
193
+ // Build request payload matching projectCreate handler
194
+ const payload = {
195
+ Company_ID: this.config.companyId,
196
+ project_name: projectName,
197
+ description: options.description || `Project for ${projectName}`,
198
+ private: options.private || false,
199
+ repo_url: options.repoUrl || null
200
+ };
201
+
202
+ const response = await this._makeApiRequest(
203
+ `${apiUrl}/api/projects`,
204
+ 'POST',
205
+ payload
206
+ );
207
+
208
+ if (response.error) {
209
+ // Handle specific error cases
210
+ if (response.status === 409) {
211
+ console.log('[Rapport] Project already exists, fetching existing...');
212
+ const existing = await this.getProject(projectName);
213
+ if (existing) return existing;
214
+ }
215
+ console.error('[Rapport] initializeProject API error:', response.error);
216
+ // Fall through to local storage
217
+ } else if (response.Records && response.Records.length > 0) {
218
+ const created = response.Records[0];
219
+
220
+ // Add collaborators if specified
221
+ if (options.collaborators && options.collaborators.length > 0) {
222
+ for (const email of options.collaborators) {
223
+ await this._addCollaborator(created.project_id, email);
224
+ }
225
+ }
226
+
227
+ return {
228
+ projectId: created.project_id,
229
+ projectName: created.project_name,
230
+ companyId: created.company_id,
231
+ description: created.description,
232
+ private: created.private,
233
+ collaborators: options.collaborators || [],
234
+ createdAt: created.created_at
235
+ };
236
+ }
237
+ } catch (error) {
238
+ console.error('[Rapport] initializeProject API call failed:', error.message);
239
+ // Fall through to local storage
240
+ }
241
+ }
242
+
243
+ // Fallback: store locally
244
+ await this.storeProjectLocally(project);
245
+ return project;
246
+ }
247
+
248
+ /**
249
+ * Add collaborator to project (internal helper)
250
+ * @private
251
+ */
252
+ async _addCollaborator(projectId, email, role = 'collaborator') {
253
+ try {
254
+ const apiUrl = this.config.apiUrl;
255
+ await this._makeApiRequest(
256
+ `${apiUrl}/api/projects/${projectId}/collaborators`,
257
+ 'POST',
258
+ { email, role }
259
+ );
260
+ } catch (error) {
261
+ console.error(`[Rapport] Failed to add collaborator ${email}:`, error.message);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Store project configuration locally
267
+ */
268
+ async storeProjectLocally(project) {
269
+ const fs = require('fs').promises;
270
+ const path = require('path');
271
+
272
+ const projectDir = path.join(__dirname, '../projects', project.projectId);
273
+ await fs.mkdir(projectDir, { recursive: true });
274
+
275
+ const configPath = path.join(projectDir, 'config.json');
276
+ await fs.writeFile(configPath, JSON.stringify(project, null, 2));
277
+
278
+ // Create collaborators file
279
+ const collabPath = path.join(projectDir, 'collaborators.json');
280
+ await fs.writeFile(collabPath, JSON.stringify({
281
+ projectId: project.projectId,
282
+ collaborators: project.collaborators,
283
+ updated: new Date().toISOString()
284
+ }, null, 2));
285
+
286
+ // Create sessions directory
287
+ const sessionsDir = path.join(projectDir, 'sessions');
288
+ await fs.mkdir(sessionsDir, { recursive: true });
289
+ }
290
+
291
+ /**
292
+ * Load project context including team patterns, load-bearing elements, and recent learning
293
+ * Fetches from API if authenticated, falls back to local storage
294
+ * @param {string} projectId - Project ID to load context for
295
+ * @param {Object} options - Context loading options
296
+ * @param {string} options.userId - User ID for personalized context
297
+ * @param {number} options.patternLimit - Max patterns to return (default 20)
298
+ * @param {number} options.learningDays - Days of recent learning to include (default 7)
299
+ * @returns {Promise<Object|null>} Project context or null if not found
300
+ */
301
+ async loadProjectContext(projectId, options = {}) {
302
+ const userId = options.userId || this.config.userId;
303
+ const patternLimit = options.patternLimit || 20;
304
+ const learningDays = options.learningDays || 7;
305
+
306
+ // Try API first if authenticated
307
+ if (this.config.authToken) {
308
+ try {
309
+ const apiUrl = this.config.apiUrl;
310
+
311
+ // Build query params
312
+ const params = new URLSearchParams();
313
+ if (userId) params.append('userId', userId);
314
+ params.append('patternLimit', patternLimit.toString());
315
+ params.append('learningDays', learningDays.toString());
316
+
317
+ const response = await this._makeApiRequest(
318
+ `${apiUrl}/api/projects/${projectId}/context?${params.toString()}`,
319
+ 'GET'
320
+ );
321
+
322
+ if (response.error) {
323
+ console.error('[Rapport] loadProjectContext API error:', response.error);
324
+ // Fall through to local storage
325
+ } else {
326
+ // Return normalized context object
327
+ return {
328
+ projectId: response.project_id || projectId,
329
+ projectName: response.project_name,
330
+ companyId: response.company_id,
331
+
332
+ // Team patterns (validated and reinforced)
333
+ patterns: (response.patterns || []).map(p => ({
334
+ patternId: p.pattern_id,
335
+ intent: p.intent,
336
+ element: p.element || p.intent,
337
+ maturity: p.maturity,
338
+ confidence: p.confidence || p.success_rate,
339
+ usageCount: p.usage_count || p.handoff_count,
340
+ lastUsed: p.last_used
341
+ })),
342
+
343
+ // Load-bearing context elements
344
+ loadBearingElements: (response.load_bearing_elements || []).map(e => ({
345
+ elementId: e.element_id,
346
+ type: e.element_type,
347
+ key: e.element_key,
348
+ value: e.element_value,
349
+ correlation: e.team_correlation,
350
+ confidence: e.confidence,
351
+ isLoadBearing: e.is_load_bearing
352
+ })),
353
+
354
+ // Recent team learning
355
+ recentLearning: (response.recent_learning || []).map(l => ({
356
+ patternId: l.pattern_id,
357
+ element: l.element,
358
+ learnedBy: l.discovered_by || l.email_address,
359
+ learnedAt: l.discovered_at || l.used_at,
360
+ success: l.success
361
+ })),
362
+
363
+ // Collaborators
364
+ collaborators: (response.collaborators || []).map(c => ({
365
+ email: c.email_address || c.email,
366
+ role: c.role,
367
+ isExternal: c.is_external,
368
+ lastActive: c.last_active
369
+ })),
370
+
371
+ // Standards applicable to this project
372
+ applicableStandards: response.applicable_standards || [],
373
+
374
+ // Metadata
375
+ lastActive: response.last_active,
376
+ sessionCount: response.session_count || 0
377
+ };
378
+ }
379
+ } catch (error) {
380
+ console.error('[Rapport] loadProjectContext API call failed:', error.message);
381
+ // Fall through to local storage
382
+ }
383
+ }
384
+
385
+ // Fallback: load from local storage
386
+ const fs = require('fs').promises;
387
+ const path = require('path');
388
+
389
+ const configPath = path.join(__dirname, '../projects', projectId, 'config.json');
390
+
391
+ try {
392
+ const content = await fs.readFile(configPath, 'utf-8');
393
+ const localProject = JSON.parse(content);
394
+
395
+ // Return minimal local context
396
+ return {
397
+ projectId: localProject.projectId,
398
+ projectName: localProject.projectName,
399
+ companyId: localProject.companyId || null,
400
+ patterns: [],
401
+ loadBearingElements: [],
402
+ recentLearning: [],
403
+ collaborators: localProject.collaborators || [],
404
+ applicableStandards: [],
405
+ lastActive: localProject.lastActive || null,
406
+ sessionCount: 0
407
+ };
408
+ } catch (error) {
409
+ return null;
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Analyze handoff (team-wide)
415
+ */
416
+ async analyzeHandoff(handoff, outcome) {
417
+ if (!this.currentProject) {
418
+ throw new Error('No project initialized. Call detectProject() first.');
419
+ }
420
+
421
+ return this.teamDetector.analyzeHandoff(
422
+ this.config.userId,
423
+ handoff,
424
+ outcome
425
+ );
426
+ }
427
+
428
+ /**
429
+ * Get team load-bearing elements
430
+ */
431
+ getTeamLoadBearing() {
432
+ return this.teamDetector.getTeamLoadBearing();
433
+ }
434
+
435
+ /**
436
+ * Get team summary
437
+ */
438
+ getTeamSummary() {
439
+ return this.teamDetector.getTeamSummary();
440
+ }
441
+
442
+ // ============================================================================
443
+ // Phase 5: Claude Code Hook Methods
444
+ // ============================================================================
445
+
446
+ /**
447
+ * Ensure standards are ingested (cached check)
448
+ * Fast execution for session-start hook
449
+ */
450
+ async ensureStandardsIngested() {
451
+ try {
452
+ const check = await this.standardsIngestion.needsIngestion();
453
+
454
+ if (check.needed) {
455
+ console.log(`[Rapport] Standards ingestion needed: ${check.reason}`);
456
+ await this.standardsIngestion.ingestEquilateralStandards();
457
+ }
458
+
459
+ return check;
460
+ } catch (error) {
461
+ console.error('[Rapport] ensureStandardsIngested error:', error.message);
462
+ return { needed: false, error: error.message };
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Get relevant standards for current project context
468
+ * Used by session-start hook
469
+ */
470
+ async getRelevantStandards(projectContext) {
471
+ try {
472
+ const context = {
473
+ workingDirectory: projectContext.workingDirectory || this.config.projectPath,
474
+ currentFile: projectContext.currentFile || null,
475
+ recentFiles: projectContext.recentFiles || [],
476
+ gitHistory: projectContext.gitHistory || null
477
+ };
478
+
479
+ return await this.relevanceDetector.detectRelevantStandards(context);
480
+ } catch (error) {
481
+ console.error('[Rapport] getRelevantStandards error:', error.message);
482
+ return { standards: [], characteristics: {}, categories: [] };
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Get recent learning for project (last N days)
488
+ */
489
+ async getRecentLearning(projectId, days = 7) {
490
+ try {
491
+ // TODO: Implement when pattern_usage table is populated
492
+ // For now return empty array
493
+ return [];
494
+ } catch (error) {
495
+ console.error('[Rapport] getRecentLearning error:', error.message);
496
+ return [];
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Detect patterns from Claude Code session transcript
502
+ * Used by pre-compact hook
503
+ */
504
+ async detectPatternsFromSession(sessionTranscript) {
505
+ try {
506
+ const patterns = [];
507
+
508
+ // Extract file changes from transcript
509
+ const files = this.extractFileChanges(sessionTranscript);
510
+
511
+ // Analyze each file for patterns
512
+ for (const file of files) {
513
+ const filePatterns = await this.extractPatternsFromFile(file);
514
+ patterns.push(...filePatterns);
515
+ }
516
+
517
+ return patterns;
518
+ } catch (error) {
519
+ console.error('[Rapport] detectPatternsFromSession error:', error.message);
520
+ return [];
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Validate pattern against standards
526
+ * Used by pre-compact hook
527
+ */
528
+ async validatePattern(pattern) {
529
+ try {
530
+ return await this.patternValidator.validatePattern(pattern);
531
+ } catch (error) {
532
+ console.error('[Rapport] validatePattern error:', error.message);
533
+ return { valid: true }; // Graceful degradation
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Record standards violation
539
+ * Stores violations in database for team learning and compliance tracking
540
+ * @param {Object} violationData - Violation data
541
+ * @param {Object} violationData.pattern - The pattern that violated standards
542
+ * @param {Array} violationData.violations - Array of violation details
543
+ * @param {string} violationData.sessionId - Session identifier
544
+ * @param {string} violationData.userId - User identifier
545
+ * @returns {Promise<Object>} Result indicating if violation was recorded
546
+ */
547
+ async recordViolation(violationData) {
548
+ try {
549
+ const apiUrl = this.config.apiUrl;
550
+
551
+ // Build request payload
552
+ const payload = {
553
+ pattern: {
554
+ element: violationData.pattern.element,
555
+ type: violationData.pattern.type,
556
+ intent: violationData.pattern.intent || violationData.pattern.element,
557
+ file: violationData.pattern.file,
558
+ category: violationData.pattern.category
559
+ },
560
+ violations: violationData.violations.map(v => ({
561
+ standard_id: v.standardId || v.standard_id,
562
+ rule: v.rule,
563
+ description: v.description || v.reason
564
+ })),
565
+ session_id: violationData.sessionId,
566
+ user_id: violationData.userId,
567
+ project_id: this.currentProject?.projectId,
568
+ timestamp: new Date().toISOString()
569
+ };
570
+
571
+ const response = await this._makeApiRequest(
572
+ `${apiUrl}/api/patterns/violations`,
573
+ 'POST',
574
+ payload
575
+ );
576
+
577
+ if (response.error) {
578
+ console.error('[Rapport] Violation recording failed:', response.error);
579
+ return { recorded: false, error: response.error };
580
+ }
581
+
582
+ console.log('[Rapport] Violation recorded:', violationData.pattern.element);
583
+ return { recorded: true, violation_id: response.violation_id };
584
+ } catch (error) {
585
+ console.error('[Rapport] recordViolation error:', error.message);
586
+ return { recorded: false, error: error.message };
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Reinforce valid pattern
592
+ * Increments usage count in database for pattern validation tracking
593
+ * @param {Object} patternData - Pattern data
594
+ * @param {Object} patternData.pattern - The pattern to reinforce
595
+ * @param {string} patternData.sessionId - Session identifier
596
+ * @param {string} patternData.userId - User identifier
597
+ * @returns {Promise<Object>} Result indicating if pattern was reinforced
598
+ */
599
+ async reinforcePattern(patternData) {
600
+ try {
601
+ const apiUrl = this.config.apiUrl;
602
+
603
+ // Build request payload
604
+ const payload = {
605
+ pattern: {
606
+ element: patternData.pattern.element,
607
+ type: patternData.pattern.type,
608
+ intent: patternData.pattern.intent || patternData.pattern.element,
609
+ file: patternData.pattern.file,
610
+ category: patternData.pattern.category,
611
+ confidence: patternData.pattern.confidence || 1.0,
612
+ evidence: patternData.pattern.evidence
613
+ },
614
+ session_id: patternData.sessionId,
615
+ user_id: patternData.userId,
616
+ project_id: this.currentProject?.projectId,
617
+ success: true,
618
+ context: {
619
+ working_directory: this.config.projectPath,
620
+ source: patternData.pattern.source || 'regex'
621
+ },
622
+ timestamp: new Date().toISOString()
623
+ };
624
+
625
+ const response = await this._makeApiRequest(
626
+ `${apiUrl}/api/patterns/usage`,
627
+ 'POST',
628
+ payload
629
+ );
630
+
631
+ if (response.error) {
632
+ console.error('[Rapport] Pattern reinforcement failed:', response.error);
633
+ return { reinforced: false, error: response.error };
634
+ }
635
+
636
+ console.log('[Rapport] Pattern reinforced:', patternData.pattern.element);
637
+ return {
638
+ reinforced: true,
639
+ usage_id: response.usage_id,
640
+ usage_count: response.usage_count,
641
+ maturity: response.maturity
642
+ };
643
+ } catch (error) {
644
+ console.error('[Rapport] reinforcePattern error:', error.message);
645
+ return { reinforced: false, error: error.message };
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Evaluate pattern for promotion to standards
651
+ * Calls CurationEngine promotion logic via API
652
+ *
653
+ * Promotion Criteria (from CurationEngine):
654
+ * - correlation >= 0.90 (90% success rate)
655
+ * - projectCount >= 5 (proven across 5+ projects)
656
+ * - developerCount >= 3 (used by 3+ developers)
657
+ * - sessionCount >= 10 (reinforced 10+ times)
658
+ *
659
+ * @param {Object} pattern - Pattern to evaluate
660
+ * @returns {Promise<Object|null>} Curation candidate if eligible, null otherwise
661
+ */
662
+ async evaluateForPromotion(pattern) {
663
+ try {
664
+ const apiUrl = this.config.apiUrl;
665
+
666
+ // Build request payload
667
+ const payload = {
668
+ pattern: {
669
+ element: pattern.element,
670
+ type: pattern.type,
671
+ intent: pattern.intent || pattern.element,
672
+ category: pattern.category,
673
+ file: pattern.file
674
+ },
675
+ project_id: this.currentProject?.projectId,
676
+ evaluate_only: true // Just check eligibility, don't create candidate yet
677
+ };
678
+
679
+ const response = await this._makeApiRequest(
680
+ `${apiUrl}/api/patterns/evaluate-promotion`,
681
+ 'POST',
682
+ payload
683
+ );
684
+
685
+ if (response.error) {
686
+ // Not an error for promotion - pattern just doesn't meet criteria
687
+ if (response.status === 404 || response.reason === 'not_eligible') {
688
+ return null;
689
+ }
690
+ console.error('[Rapport] Pattern promotion evaluation failed:', response.error);
691
+ return null;
692
+ }
693
+
694
+ // Pattern is eligible for promotion
695
+ if (response.eligible) {
696
+ console.log(`[Rapport] Pattern eligible for promotion: ${pattern.element}`);
697
+ return {
698
+ element: pattern.element,
699
+ patternId: response.pattern_id,
700
+ correlation: response.metrics?.success_correlation || response.correlation,
701
+ projectCount: response.metrics?.project_count,
702
+ developerCount: response.metrics?.developer_count,
703
+ sessionCount: response.metrics?.session_count,
704
+ category: response.proposed_category || pattern.category,
705
+ candidateId: response.candidate_id
706
+ };
707
+ }
708
+
709
+ return null;
710
+ } catch (error) {
711
+ console.error('[Rapport] evaluateForPromotion error:', error.message);
712
+ return null;
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Record standards shown during session (Phase 7)
718
+ * Fire-and-forget to avoid blocking session-start hook
719
+ * @param {string} sessionId - Session identifier
720
+ * @param {Array} standards - Array of standards that were injected
721
+ */
722
+ recordStandardsShown(sessionId, standards) {
723
+ // Fire and forget - don't await to keep hook fast
724
+ this._recordStandardsAsync(sessionId, standards).catch(err => {
725
+ console.error('[Rapport] recordStandardsShown error:', err.message);
726
+ });
727
+ }
728
+
729
+ async _recordStandardsAsync(sessionId, standards) {
730
+ if (!standards || standards.length === 0) return;
731
+
732
+ const apiUrl = this.config.apiUrl;
733
+ const https = require('https');
734
+ const url = require('url');
735
+
736
+ // Prepare payload
737
+ const payload = JSON.stringify({
738
+ session_id: sessionId,
739
+ standards: standards.map(s => ({
740
+ pattern_id: s.pattern_id || s.element,
741
+ relevance_score: s.score || 0
742
+ }))
743
+ });
744
+
745
+ // Non-blocking HTTP request
746
+ const parsedUrl = url.parse(`${apiUrl}/api/sessions/standards`);
747
+
748
+ const options = {
749
+ hostname: parsedUrl.hostname,
750
+ port: parsedUrl.port || 443,
751
+ path: parsedUrl.path,
752
+ method: 'POST',
753
+ headers: {
754
+ 'Content-Type': 'application/json',
755
+ 'Content-Length': Buffer.byteLength(payload)
756
+ },
757
+ timeout: 2000 // 2s timeout
758
+ };
759
+
760
+ return new Promise((resolve, reject) => {
761
+ const req = https.request(options, (res) => {
762
+ resolve({ statusCode: res.statusCode });
763
+ });
764
+
765
+ req.on('error', (err) => {
766
+ // Log but don't fail - graceful degradation
767
+ console.error('[Rapport] API call failed:', err.message);
768
+ resolve({ error: err.message });
769
+ });
770
+
771
+ req.on('timeout', () => {
772
+ req.destroy();
773
+ resolve({ error: 'timeout' });
774
+ });
775
+
776
+ req.write(payload);
777
+ req.end();
778
+ });
779
+ }
780
+
781
+ /**
782
+ * Generate session ID for tracking
783
+ */
784
+ generateSessionId() {
785
+ const crypto = require('crypto');
786
+ const timestamp = Date.now().toString(36);
787
+ const random = crypto.randomBytes(4).toString('hex');
788
+ return `ses_${timestamp}_${random}`;
789
+ }
790
+
791
+ // ============================================================================
792
+ // Helper Methods
793
+ // ============================================================================
794
+
795
+ /**
796
+ * Make authenticated API request
797
+ * @private
798
+ * @param {string} url - Full URL to request
799
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
800
+ * @param {Object} body - Request body (for POST/PUT)
801
+ * @param {number} timeout - Request timeout in milliseconds
802
+ * @returns {Promise<Object>} Parsed JSON response or error object
803
+ */
804
+ async _makeApiRequest(url, method = 'GET', body = null, timeout = 10000) {
805
+ const https = require('https');
806
+ const http = require('http');
807
+ const urlModule = require('url');
808
+
809
+ const parsedUrl = urlModule.parse(url);
810
+ const isHttps = parsedUrl.protocol === 'https:';
811
+ const httpModule = isHttps ? https : http;
812
+
813
+ const payload = body ? JSON.stringify(body) : null;
814
+
815
+ const options = {
816
+ hostname: parsedUrl.hostname,
817
+ port: parsedUrl.port || (isHttps ? 443 : 80),
818
+ path: parsedUrl.path,
819
+ method: method,
820
+ headers: {
821
+ 'Content-Type': 'application/json',
822
+ 'Accept': 'application/json'
823
+ },
824
+ timeout: timeout
825
+ };
826
+
827
+ // Add auth header if token is available
828
+ if (this.config.authToken) {
829
+ options.headers['Authorization'] = `Bearer ${this.config.authToken}`;
830
+ }
831
+
832
+ // Add content length for POST/PUT
833
+ if (payload) {
834
+ options.headers['Content-Length'] = Buffer.byteLength(payload);
835
+ }
836
+
837
+ return new Promise((resolve) => {
838
+ const req = httpModule.request(options, (res) => {
839
+ let data = '';
840
+
841
+ res.on('data', (chunk) => {
842
+ data += chunk;
843
+ });
844
+
845
+ res.on('end', () => {
846
+ try {
847
+ const parsed = JSON.parse(data);
848
+
849
+ // Check for error status codes
850
+ if (res.statusCode >= 400) {
851
+ resolve({
852
+ error: parsed.message || parsed.error || `HTTP ${res.statusCode}`,
853
+ status: res.statusCode,
854
+ ...parsed
855
+ });
856
+ } else {
857
+ resolve(parsed);
858
+ }
859
+ } catch (e) {
860
+ // Non-JSON response
861
+ if (res.statusCode >= 400) {
862
+ resolve({
863
+ error: `HTTP ${res.statusCode}: ${data}`,
864
+ status: res.statusCode
865
+ });
866
+ } else {
867
+ resolve({ data: data, status: res.statusCode });
868
+ }
869
+ }
870
+ });
871
+ });
872
+
873
+ req.on('error', (err) => {
874
+ console.error('[Rapport] API request error:', err.message);
875
+ resolve({ error: err.message });
876
+ });
877
+
878
+ req.on('timeout', () => {
879
+ req.destroy();
880
+ resolve({ error: 'Request timeout' });
881
+ });
882
+
883
+ if (payload) {
884
+ req.write(payload);
885
+ }
886
+
887
+ req.end();
888
+ });
889
+ }
890
+
891
+ /**
892
+ * Extract file changes from session transcript
893
+ */
894
+ extractFileChanges(sessionTranscript) {
895
+ const files = [];
896
+
897
+ // Simple extraction - look for common patterns
898
+ if (sessionTranscript.transcript) {
899
+ const matches = sessionTranscript.transcript.match(/(?:created|modified|updated)\s+([^\s]+\.(?:js|ts|tsx|jsx|sql|yaml))/gi);
900
+ if (matches) {
901
+ matches.forEach(match => {
902
+ const file = match.match(/([^\s]+\.(?:js|ts|tsx|jsx|sql|yaml))/i);
903
+ if (file) {
904
+ files.push(file[1]);
905
+ }
906
+ });
907
+ }
908
+ }
909
+
910
+ return [...new Set(files)]; // Deduplicate
911
+ }
912
+
913
+ /**
914
+ * Extract patterns from a file
915
+ */
916
+ async extractPatternsFromFile(filePath) {
917
+ try {
918
+ const fs = require('fs').promises;
919
+ const path = require('path');
920
+
921
+ const fullPath = path.join(this.config.projectPath, filePath);
922
+ const content = await fs.readFile(fullPath, 'utf-8');
923
+
924
+ const patterns = [];
925
+
926
+ // Detect common patterns
927
+ if (content.includes('wrapHandler')) {
928
+ patterns.push({
929
+ element: 'Lambda Handler Pattern',
930
+ rule: 'Use wrapHandler for Lambda functions',
931
+ file: filePath
932
+ });
933
+ }
934
+
935
+ if (content.includes('executeQuery')) {
936
+ patterns.push({
937
+ element: 'Database Query Pattern',
938
+ rule: 'Use executeQuery helper',
939
+ file: filePath
940
+ });
941
+ }
942
+
943
+ if (content.includes('createSuccessResponse')) {
944
+ patterns.push({
945
+ element: 'API Response Pattern',
946
+ rule: 'Use createSuccessResponse helper',
947
+ file: filePath
948
+ });
949
+ }
950
+
951
+ return patterns;
952
+ } catch (error) {
953
+ return [];
954
+ }
955
+ }
956
+ }
957
+
958
+ // Backward compatibility alias
959
+ const RapportClient = MindmeldClient;
960
+
961
+ module.exports = {
962
+ MindmeldClient,
963
+ RapportClient,
964
+ TeamLoadBearingDetector,
965
+ CollaborationPrompt,
966
+ RelevanceDetector,
967
+ PatternValidator,
968
+ StandardsIngestion
969
+ };