@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,575 @@
1
+ /**
2
+ * StandardsIngestion.js - Phase 1 of Rapport Standards Integration
3
+ *
4
+ * Parses and ingests .equilateral-standards/ markdown files into Rapport database
5
+ * Creates standards_patterns from enforcement rules, anti-patterns, examples, and cost impacts
6
+ *
7
+ * Reference: /Users/jamesford/Source/rapport/docs/RAPPORT_STANDARDS_INTEGRATION_DESIGN.md
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+ const { executeQuery } = require('../handlers/helpers/dbOperations');
13
+
14
+ class StandardsIngestion {
15
+ constructor(options = {}) {
16
+ this.standardsPath = options.standardsPath || path.join(process.cwd(), '.equilateral-standards');
17
+ this.verbose = options.verbose || false;
18
+ }
19
+
20
+ /**
21
+ * Main ingestion entry point
22
+ * Discovers and parses all .equilateral-standards/ markdown files
23
+ */
24
+ async ingestEquilateralStandards() {
25
+ console.log(`[StandardsIngestion] Starting ingestion from ${this.standardsPath}`);
26
+
27
+ const startTime = Date.now();
28
+ const patterns = [];
29
+
30
+ // Define standard categories
31
+ const categories = [
32
+ 'serverless-saas-aws',
33
+ 'multi-agent-orchestration',
34
+ 'compliance-security',
35
+ 'database',
36
+ 'frontend-development',
37
+ 'cost-optimization',
38
+ 'backend',
39
+ 'deployment',
40
+ 'real-time-systems',
41
+ 'testing',
42
+ 'well-architected'
43
+ ];
44
+
45
+ // Discover and parse all standards files
46
+ for (const category of categories) {
47
+ const categoryPath = path.join(this.standardsPath, category);
48
+
49
+ try {
50
+ const files = await this.discoverStandardFiles(categoryPath);
51
+
52
+ for (const file of files) {
53
+ try {
54
+ const standard = await this.parseStandard(file, category);
55
+
56
+ if (standard && standard.patterns.length > 0) {
57
+ patterns.push(...standard.patterns);
58
+
59
+ if (this.verbose) {
60
+ console.log(` ✓ Parsed ${path.basename(file)}: ${standard.patterns.length} patterns`);
61
+ }
62
+ }
63
+ } catch (parseError) {
64
+ console.warn(` ⚠ Failed to parse ${path.basename(file)}:`, parseError.message);
65
+ }
66
+ }
67
+ } catch (categoryError) {
68
+ if (categoryError.code !== 'ENOENT') {
69
+ console.warn(` ⚠ Failed to process category ${category}:`, categoryError.message);
70
+ }
71
+ }
72
+ }
73
+
74
+ // Store in database
75
+ const ingestedCount = await this.seedPatternDatabase(patterns);
76
+
77
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
78
+
79
+ return {
80
+ patternsIngested: ingestedCount,
81
+ totalPatternsFound: patterns.length,
82
+ categories: categories.length,
83
+ timestamp: new Date().toISOString(),
84
+ duration: `${duration}s`
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Discover all markdown files in a category directory
90
+ */
91
+ async discoverStandardFiles(categoryPath) {
92
+ try {
93
+ const entries = await fs.readdir(categoryPath, { withFileTypes: true });
94
+ const files = [];
95
+
96
+ for (const entry of entries) {
97
+ const fullPath = path.join(categoryPath, entry.name);
98
+
99
+ if (entry.isDirectory()) {
100
+ // Recursively search subdirectories
101
+ const subFiles = await this.discoverStandardFiles(fullPath);
102
+ files.push(...subFiles);
103
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
104
+ files.push(fullPath);
105
+ }
106
+ }
107
+
108
+ return files;
109
+ } catch (error) {
110
+ if (error.code === 'ENOENT') {
111
+ // Category doesn't exist - not an error
112
+ return [];
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Parse a markdown standards file into Rapport pattern format
120
+ */
121
+ async parseStandard(filepath, category) {
122
+ const content = await fs.readFile(filepath, 'utf-8');
123
+ const filename = path.basename(filepath, '.md');
124
+
125
+ // Extract standard metadata from markdown
126
+ const title = this.extractTitle(content);
127
+ const enforcementRules = this.extractEnforcementRules(content);
128
+ const antiPatterns = this.extractAntiPatterns(content);
129
+ const examples = this.extractExamples(content);
130
+ const costImpact = this.extractCostImpact(content);
131
+ const applicableFiles = this.extractApplicableFiles(content, category);
132
+
133
+ const patterns = [];
134
+
135
+ // Create pattern for each enforcement rule
136
+ for (const rule of enforcementRules) {
137
+ const patternId = this.generatePatternId(category, filename, rule.element);
138
+
139
+ patterns.push({
140
+ pattern_id: patternId,
141
+ element: rule.element,
142
+ rule: rule.rule,
143
+ correlation: 1.0, // Standards are mandatory
144
+ source: 'equilateral-standards',
145
+ maturity: rule.severity === 'MANDATORY' || rule.severity === 'CRITICAL' ? 'enforced' : 'validated',
146
+ scope: 'organization',
147
+ category: category,
148
+ applicable_files: applicableFiles,
149
+ anti_patterns: antiPatterns,
150
+ examples: examples,
151
+ cost_impact: costImpact,
152
+ source_file: filepath,
153
+ title: title
154
+ });
155
+ }
156
+
157
+ return { patterns };
158
+ }
159
+
160
+ /**
161
+ * Extract title from markdown (first H1)
162
+ */
163
+ extractTitle(content) {
164
+ const match = content.match(/^#\s+(.+)$/m);
165
+ return match ? match[1].trim() : 'Untitled Standard';
166
+ }
167
+
168
+ /**
169
+ * Extract enforcement rules from markdown
170
+ * Looks for sections like "MANDATORY PATTERNS", "Core Principles", etc.
171
+ */
172
+ extractEnforcementRules(content) {
173
+ const rules = [];
174
+
175
+ // Pattern 1: MANDATORY/CRITICAL sections
176
+ const mandatorySection = content.match(/##?\s*⚠?️?\s*(MANDATORY|CRITICAL)[^#]*/gi);
177
+ if (mandatorySection) {
178
+ for (const section of mandatorySection) {
179
+ const sectionRules = this.extractRulesFromSection(section, 'MANDATORY');
180
+ rules.push(...sectionRules);
181
+ }
182
+ }
183
+
184
+ // Pattern 2: "ALWAYS" and "NEVER" statements
185
+ const alwaysNever = content.match(/(?:ALWAYS|NEVER)\s+([^.\n]+)/g);
186
+ if (alwaysNever) {
187
+ for (const statement of alwaysNever) {
188
+ rules.push({
189
+ element: this.extractElement(statement),
190
+ rule: statement.trim(),
191
+ severity: 'MANDATORY'
192
+ });
193
+ }
194
+ }
195
+
196
+ // Pattern 3: Core Principles
197
+ const principleMatch = content.match(/##?\s*Core Principle[s]?[:\s]+([^#]+)/i);
198
+ if (principleMatch) {
199
+ rules.push({
200
+ element: 'Core Principle',
201
+ rule: principleMatch[1].trim(),
202
+ severity: 'ENFORCED'
203
+ });
204
+ }
205
+
206
+ return rules.length > 0 ? rules : [{
207
+ element: 'Standard Pattern',
208
+ rule: this.extractFirstParagraph(content),
209
+ severity: 'VALIDATED'
210
+ }];
211
+ }
212
+
213
+ /**
214
+ * Extract rules from a section of content
215
+ */
216
+ extractRulesFromSection(section, severity) {
217
+ const rules = [];
218
+
219
+ // Look for numbered or bulleted lists
220
+ const listItems = section.match(/^[\s]*[-*\d]+\.?\s+(.+)$/gm);
221
+ if (listItems) {
222
+ for (const item of listItems) {
223
+ const cleaned = item.replace(/^[\s]*[-*\d]+\.?\s+/, '').trim();
224
+ if (cleaned.length > 10) { // Filter out very short items
225
+ rules.push({
226
+ element: this.extractElement(cleaned),
227
+ rule: cleaned,
228
+ severity: severity
229
+ });
230
+ }
231
+ }
232
+ }
233
+
234
+ // Look for bold statements
235
+ const boldStatements = section.match(/\*\*(.+?)\*\*/g);
236
+ if (boldStatements && rules.length === 0) {
237
+ for (const statement of boldStatements) {
238
+ const cleaned = statement.replace(/\*\*/g, '').trim();
239
+ if (cleaned.length > 10 && !cleaned.match(/^(MANDATORY|CRITICAL|NOTE|IMPORTANT)/i)) {
240
+ rules.push({
241
+ element: this.extractElement(cleaned),
242
+ rule: cleaned,
243
+ severity: severity
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ return rules;
250
+ }
251
+
252
+ /**
253
+ * Extract anti-patterns from markdown
254
+ * Looks for ❌ WRONG, NEVER DO THIS, Anti-Pattern sections
255
+ */
256
+ extractAntiPatterns(content) {
257
+ const antiPatterns = [];
258
+
259
+ // Pattern 1: ❌ markers
260
+ const wrongPatterns = content.match(/❌[^```]*?```[^`]+```/gs);
261
+ if (wrongPatterns) {
262
+ for (const pattern of wrongPatterns) {
263
+ const description = pattern.match(/❌\s*([^\n`]+)/);
264
+ const code = pattern.match(/```(\w+)?\n([\s\S]+?)```/);
265
+
266
+ if (description && code) {
267
+ antiPatterns.push({
268
+ description: description[1].trim(),
269
+ code: code[2].trim(),
270
+ language: code[1] || 'javascript'
271
+ });
272
+ }
273
+ }
274
+ }
275
+
276
+ // Pattern 2: Anti-Pattern sections
277
+ const antiPatternSection = content.match(/##?\s*Anti-Pattern[s]?[^#]*/gi);
278
+ if (antiPatternSection) {
279
+ for (const section of antiPatternSection) {
280
+ const sectionPatterns = this.extractCodeBlocksFromSection(section);
281
+ antiPatterns.push(...sectionPatterns.map(p => ({
282
+ description: p.description || 'Anti-pattern to avoid',
283
+ code: p.code,
284
+ language: p.language
285
+ })));
286
+ }
287
+ }
288
+
289
+ return antiPatterns;
290
+ }
291
+
292
+ /**
293
+ * Extract examples from markdown
294
+ * Looks for ✅ CORRECT, Example sections
295
+ */
296
+ extractExamples(content) {
297
+ const examples = [];
298
+
299
+ // Pattern 1: ✅ markers
300
+ const correctPatterns = content.match(/✅[^```]*?```[^`]+```/gs);
301
+ if (correctPatterns) {
302
+ for (const pattern of correctPatterns) {
303
+ const description = pattern.match(/✅\s*([^\n`]+)/);
304
+ const code = pattern.match(/```(\w+)?\n([\s\S]+?)```/);
305
+
306
+ if (description && code) {
307
+ examples.push({
308
+ description: description[1].trim(),
309
+ code: code[2].trim(),
310
+ language: code[1] || 'javascript'
311
+ });
312
+ }
313
+ }
314
+ }
315
+
316
+ // Pattern 2: Example sections
317
+ const exampleSection = content.match(/##?\s*Example[s]?[^#]*/gi);
318
+ if (exampleSection) {
319
+ for (const section of exampleSection) {
320
+ const sectionExamples = this.extractCodeBlocksFromSection(section);
321
+ examples.push(...sectionExamples.map(p => ({
322
+ description: p.description || 'Example implementation',
323
+ code: p.code,
324
+ language: p.language
325
+ })));
326
+ }
327
+ }
328
+
329
+ return examples;
330
+ }
331
+
332
+ /**
333
+ * Extract cost impact information
334
+ */
335
+ extractCostImpact(content) {
336
+ const costImpact = {};
337
+
338
+ // Look for cost comparison tables
339
+ const costTable = content.match(/\|[^|]*Cost[^|]*\|[\s\S]*?\n\n/i);
340
+ if (costTable) {
341
+ costImpact.hasTable = true;
342
+ costImpact.details = costTable[0].trim();
343
+ }
344
+
345
+ // Look for specific cost mentions
346
+ const costMentions = content.match(/\$\d+[^.\n]*/g);
347
+ if (costMentions) {
348
+ costImpact.mentions = costMentions.map(m => m.trim());
349
+ }
350
+
351
+ // Look for "Cost Impact" sections
352
+ const costSection = content.match(/##?\s*Cost Impact[^#]*/i);
353
+ if (costSection) {
354
+ costImpact.section = costSection[0].trim();
355
+ }
356
+
357
+ return Object.keys(costImpact).length > 0 ? costImpact : null;
358
+ }
359
+
360
+ /**
361
+ * Determine applicable file patterns based on category and content
362
+ */
363
+ extractApplicableFiles(content, category) {
364
+ const patterns = [];
365
+
366
+ // Category-based patterns
367
+ const categoryPatterns = {
368
+ 'serverless-saas-aws': ['**/*.js', '**/template.yaml', '**/handlers/**/*.js'],
369
+ 'frontend-development': ['**/*.tsx', '**/*.jsx', '**/*.ts', 'src/frontend/**/*'],
370
+ 'database': ['**/*.sql', '**/dbOperations.js', '**/migrations/**/*'],
371
+ 'backend': ['src/backend/**/*.js', 'src/handlers/**/*.js'],
372
+ 'multi-agent-orchestration': ['src/agents/**/*.js', 'src/core/**/*.js']
373
+ };
374
+
375
+ if (categoryPatterns[category]) {
376
+ patterns.push(...categoryPatterns[category]);
377
+ }
378
+
379
+ // Content-based patterns
380
+ if (content.includes('Lambda') || content.includes('handler')) {
381
+ patterns.push('src/handlers/**/*.js');
382
+ }
383
+ if (content.includes('React') || content.includes('component')) {
384
+ patterns.push('src/frontend/**/*.tsx', 'src/frontend/**/*.jsx');
385
+ }
386
+ if (content.includes('PostgreSQL') || content.includes('database')) {
387
+ patterns.push('**/*dbOperations.js', '**/*.sql');
388
+ }
389
+
390
+ return [...new Set(patterns)]; // Remove duplicates
391
+ }
392
+
393
+ /**
394
+ * Extract code blocks from a section
395
+ */
396
+ extractCodeBlocksFromSection(section) {
397
+ const blocks = [];
398
+ const codeBlocks = section.match(/```(\w+)?\n([\s\S]+?)```/g);
399
+
400
+ if (codeBlocks) {
401
+ for (const block of codeBlocks) {
402
+ const match = block.match(/```(\w+)?\n([\s\S]+?)```/);
403
+ if (match) {
404
+ // Look for description before code block
405
+ const beforeBlock = section.substring(0, section.indexOf(block));
406
+ const descMatch = beforeBlock.match(/([^\n]+)\n*$/);
407
+
408
+ blocks.push({
409
+ description: descMatch ? descMatch[1].trim() : null,
410
+ code: match[2].trim(),
411
+ language: match[1] || 'javascript'
412
+ });
413
+ }
414
+ }
415
+ }
416
+
417
+ return blocks;
418
+ }
419
+
420
+ /**
421
+ * Extract element name from a rule statement
422
+ */
423
+ extractElement(statement) {
424
+ // Remove modifiers
425
+ let cleaned = statement.replace(/^(ALWAYS|NEVER|DO NOT|DON'T)\s+/i, '');
426
+
427
+ // Extract first noun phrase (simple heuristic)
428
+ const match = cleaned.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/);
429
+ if (match) {
430
+ return match[1];
431
+ }
432
+
433
+ // Extract from "Use X" or "Implement X"
434
+ const actionMatch = cleaned.match(/^(?:Use|Implement|Apply|Follow|Add|Create)\s+([^,.\n]+)/i);
435
+ if (actionMatch) {
436
+ return actionMatch[1].trim();
437
+ }
438
+
439
+ // Fallback: first few words
440
+ const words = cleaned.split(/\s+/).slice(0, 3);
441
+ return words.join(' ');
442
+ }
443
+
444
+ /**
445
+ * Extract first paragraph from content
446
+ */
447
+ extractFirstParagraph(content) {
448
+ const paragraphs = content.split(/\n\n+/);
449
+ for (const para of paragraphs) {
450
+ const cleaned = para.replace(/^#+\s+/, '').trim();
451
+ if (cleaned.length > 20 && !cleaned.startsWith('**')) {
452
+ return cleaned;
453
+ }
454
+ }
455
+ return 'Standard pattern';
456
+ }
457
+
458
+ /**
459
+ * Generate unique pattern ID
460
+ */
461
+ generatePatternId(category, filename, element) {
462
+ const categorySlug = category.replace(/[^a-z0-9]/gi, '_');
463
+ const fileSlug = filename.replace(/[^a-z0-9]/gi, '_');
464
+ const elementSlug = element.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 30);
465
+
466
+ return `${categorySlug}_${fileSlug}_${elementSlug}`.toLowerCase();
467
+ }
468
+
469
+ /**
470
+ * Store patterns in database
471
+ */
472
+ async seedPatternDatabase(patterns) {
473
+ if (patterns.length === 0) {
474
+ console.log('[StandardsIngestion] No patterns to ingest');
475
+ return 0;
476
+ }
477
+
478
+ console.log(`[StandardsIngestion] Storing ${patterns.length} patterns in database...`);
479
+
480
+ let successCount = 0;
481
+
482
+ for (const pattern of patterns) {
483
+ try {
484
+ await executeQuery(`
485
+ INSERT INTO rapport.standards_patterns (
486
+ pattern_id,
487
+ element,
488
+ rule,
489
+ correlation,
490
+ source,
491
+ maturity,
492
+ scope,
493
+ category,
494
+ applicable_files,
495
+ anti_patterns,
496
+ examples,
497
+ cost_impact,
498
+ created_at,
499
+ last_updated
500
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
501
+ ON CONFLICT (pattern_id)
502
+ DO UPDATE SET
503
+ element = EXCLUDED.element,
504
+ rule = EXCLUDED.rule,
505
+ correlation = EXCLUDED.correlation,
506
+ maturity = EXCLUDED.maturity,
507
+ scope = EXCLUDED.scope,
508
+ category = EXCLUDED.category,
509
+ applicable_files = EXCLUDED.applicable_files,
510
+ anti_patterns = EXCLUDED.anti_patterns,
511
+ examples = EXCLUDED.examples,
512
+ cost_impact = EXCLUDED.cost_impact,
513
+ last_updated = NOW()
514
+ `, [
515
+ pattern.pattern_id,
516
+ pattern.element,
517
+ pattern.rule,
518
+ pattern.correlation,
519
+ pattern.source,
520
+ pattern.maturity,
521
+ pattern.scope,
522
+ pattern.category,
523
+ pattern.applicable_files,
524
+ JSON.stringify(pattern.anti_patterns),
525
+ JSON.stringify(pattern.examples),
526
+ pattern.cost_impact ? JSON.stringify(pattern.cost_impact) : null
527
+ ]);
528
+
529
+ successCount++;
530
+
531
+ if (this.verbose && successCount % 10 === 0) {
532
+ console.log(` Stored ${successCount}/${patterns.length} patterns...`);
533
+ }
534
+ } catch (error) {
535
+ console.error(` ✗ Failed to store pattern ${pattern.pattern_id}:`, error.message);
536
+ }
537
+ }
538
+
539
+ console.log(`[StandardsIngestion] ✓ Successfully stored ${successCount} patterns`);
540
+ return successCount;
541
+ }
542
+
543
+ /**
544
+ * Check if standards need re-ingestion (for performance)
545
+ */
546
+ async needsIngestion() {
547
+ try {
548
+ const result = await executeQuery(`
549
+ SELECT COUNT(*) as count, MAX(last_updated) as last_updated
550
+ FROM rapport.standards_patterns
551
+ WHERE source = 'equilateral-standards'
552
+ `);
553
+
554
+ const row = result.rows[0];
555
+
556
+ // Re-ingest if no patterns exist or haven't been updated in 7 days
557
+ if (row.count === '0') {
558
+ return { needed: true, reason: 'No standards ingested yet' };
559
+ }
560
+
561
+ const lastUpdated = new Date(row.last_updated);
562
+ const daysSince = (Date.now() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24);
563
+
564
+ if (daysSince > 7) {
565
+ return { needed: true, reason: `Last updated ${daysSince.toFixed(0)} days ago` };
566
+ }
567
+
568
+ return { needed: false, count: row.count };
569
+ } catch (error) {
570
+ return { needed: true, reason: 'Database check failed' };
571
+ }
572
+ }
573
+ }
574
+
575
+ module.exports = { StandardsIngestion };