@equilateral_ai/mindmeld 3.1.2 → 3.3.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 (71) hide show
  1. package/README.md +4 -4
  2. package/hooks/README.md +46 -4
  3. package/hooks/pre-compact.js +115 -12
  4. package/hooks/session-end.js +292 -0
  5. package/hooks/session-start.js +302 -25
  6. package/package.json +5 -2
  7. package/scripts/auth-login.js +53 -0
  8. package/scripts/harvest.js +59 -19
  9. package/scripts/init-project.js +134 -374
  10. package/scripts/inject.js +30 -9
  11. package/scripts/repo-analyzer.js +870 -0
  12. package/src/core/AuthManager.js +498 -0
  13. package/src/core/CrossReferenceEngine.js +624 -0
  14. package/src/core/DeprecationScheduler.js +183 -0
  15. package/src/core/LLMPatternDetector.js +218 -0
  16. package/src/core/RapportOrchestrator.js +186 -0
  17. package/src/core/RelevanceDetector.js +32 -2
  18. package/src/core/StandardLifecycle.js +244 -0
  19. package/src/core/StandardsIngestion.js +341 -28
  20. package/src/core/parsers/adrParser.js +479 -0
  21. package/src/core/parsers/cursorRulesParser.js +564 -0
  22. package/src/core/parsers/eslintParser.js +439 -0
  23. package/src/handlers/alerts/alertsAcknowledge.js +4 -3
  24. package/src/handlers/analytics/activitySummaryGet.js +235 -0
  25. package/src/handlers/analytics/coachingGet.js +361 -0
  26. package/src/handlers/analytics/developerScoreGet.js +207 -0
  27. package/src/handlers/collaborators/collaboratorAdd.js +4 -5
  28. package/src/handlers/collaborators/collaboratorInvite.js +6 -5
  29. package/src/handlers/collaborators/collaboratorList.js +3 -3
  30. package/src/handlers/collaborators/collaboratorRemove.js +5 -4
  31. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
  32. package/src/handlers/correlations/correlationsGet.js +1 -1
  33. package/src/handlers/correlations/correlationsProjectGet.js +7 -6
  34. package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
  35. package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
  36. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
  37. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
  38. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
  39. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
  40. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
  41. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
  42. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
  43. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
  44. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
  45. package/src/handlers/github/githubConnectionStatus.js +1 -1
  46. package/src/handlers/github/githubDiscoverPatterns.js +264 -5
  47. package/src/handlers/github/githubOAuthCallback.js +14 -2
  48. package/src/handlers/github/githubOAuthStart.js +1 -1
  49. package/src/handlers/github/githubPatternsReview.js +1 -1
  50. package/src/handlers/github/githubReposList.js +1 -1
  51. package/src/handlers/helpers/auditLogger.js +201 -0
  52. package/src/handlers/helpers/index.js +19 -1
  53. package/src/handlers/helpers/lambdaWrapper.js +1 -1
  54. package/src/handlers/notifications/sendNotification.js +1 -1
  55. package/src/handlers/projects/projectCreate.js +28 -1
  56. package/src/handlers/projects/projectDelete.js +3 -3
  57. package/src/handlers/projects/projectUpdate.js +4 -5
  58. package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
  59. package/src/handlers/scheduled/generateAlerts.js +1 -1
  60. package/src/handlers/standards/catalogGet.js +185 -0
  61. package/src/handlers/standards/catalogSync.js +120 -0
  62. package/src/handlers/standards/projectStandardsGet.js +135 -0
  63. package/src/handlers/standards/projectStandardsPut.js +131 -0
  64. package/src/handlers/standards/standardsAuditGet.js +65 -0
  65. package/src/handlers/standards/standardsParseUpload.js +153 -0
  66. package/src/handlers/standards/standardsRelevantPost.js +213 -0
  67. package/src/handlers/standards/standardsTransition.js +64 -0
  68. package/src/handlers/user/userSplashAck.js +91 -0
  69. package/src/handlers/user/userSplashGet.js +194 -0
  70. package/src/handlers/users/userProfilePut.js +77 -0
  71. package/src/index.js +37 -29
@@ -1,14 +1,23 @@
1
1
  /**
2
2
  * StandardsIngestion.js - Phase 1 of Rapport Standards Integration
3
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
4
+ * Parses and ingests .equilateral-standards/ YAML files into Rapport database
5
+ * Creates standards_patterns from rules, anti-patterns, examples, and cost impacts
6
+ *
7
+ * YAML Schema (equilateral-standards-yaml):
8
+ * id, category, priority (10/20/30), updated
9
+ * rules: [{ action: ALWAYS|NEVER|USE|PREFER|AVOID, rule: string, applies_to: [globs] }]
10
+ * anti_patterns: [strings]
11
+ * examples: { name: code }
12
+ * cost_impact: { pattern: impact }
13
+ * tags: [strings]
6
14
  *
7
15
  * Reference: /Users/jamesford/Source/rapport/docs/RAPPORT_STANDARDS_INTEGRATION_DESIGN.md
8
16
  */
9
17
 
10
18
  const fs = require('fs').promises;
11
19
  const path = require('path');
20
+ const yaml = require('js-yaml');
12
21
  const { executeQuery } = require('../handlers/helpers/dbOperations');
13
22
 
14
23
  class StandardsIngestion {
@@ -19,7 +28,7 @@ class StandardsIngestion {
19
28
 
20
29
  /**
21
30
  * Main ingestion entry point
22
- * Discovers and parses all .equilateral-standards/ markdown files
31
+ * Discovers and parses all .equilateral-standards/ YAML files (with markdown fallback)
23
32
  */
24
33
  async ingestEquilateralStandards() {
25
34
  console.log(`[StandardsIngestion] Starting ingestion from ${this.standardsPath}`);
@@ -86,12 +95,61 @@ class StandardsIngestion {
86
95
  }
87
96
 
88
97
  /**
89
- * Discover all markdown files in a category directory
98
+ * Parse all standards from YAML files without storing in database.
99
+ * Used as a fallback when the database is unavailable (e.g., local dev).
100
+ * @returns {Promise<Array>} Parsed patterns array
101
+ */
102
+ async parseAllStandards() {
103
+ const patterns = [];
104
+
105
+ const categories = [
106
+ 'serverless-saas-aws',
107
+ 'multi-agent-orchestration',
108
+ 'compliance-security',
109
+ 'database',
110
+ 'frontend-development',
111
+ 'cost-optimization',
112
+ 'backend',
113
+ 'deployment',
114
+ 'real-time-systems',
115
+ 'testing',
116
+ 'well-architected'
117
+ ];
118
+
119
+ for (const category of categories) {
120
+ const categoryPath = path.join(this.standardsPath, category);
121
+
122
+ try {
123
+ const files = await this.discoverStandardFiles(categoryPath);
124
+
125
+ for (const file of files) {
126
+ try {
127
+ const standard = await this.parseStandard(file, category);
128
+ if (standard && standard.patterns.length > 0) {
129
+ patterns.push(...standard.patterns);
130
+ }
131
+ } catch (parseError) {
132
+ // Skip unparseable files
133
+ }
134
+ }
135
+ } catch (categoryError) {
136
+ if (categoryError.code !== 'ENOENT') {
137
+ // Skip missing categories silently
138
+ }
139
+ }
140
+ }
141
+
142
+ return patterns;
143
+ }
144
+
145
+ /**
146
+ * Discover all standard files in a category directory
147
+ * Prefers YAML files over markdown - if both exist for the same base name, only YAML is returned
90
148
  */
91
149
  async discoverStandardFiles(categoryPath) {
92
150
  try {
93
151
  const entries = await fs.readdir(categoryPath, { withFileTypes: true });
94
- const files = [];
152
+ const fileMap = new Map(); // baseName -> { yaml: path, md: path }
95
153
 
96
154
  for (const entry of entries) {
97
155
  const fullPath = path.join(categoryPath, entry.name);
@@ -99,9 +157,47 @@ class StandardsIngestion {
99
157
  if (entry.isDirectory()) {
100
158
  // Recursively search subdirectories
101
159
  const subFiles = await this.discoverStandardFiles(fullPath);
102
- files.push(...subFiles);
103
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
104
- files.push(fullPath);
160
+ // Add subdirectory files directly (they've already been deduplicated)
161
+ for (const subFile of subFiles) {
162
+ const subBaseName = path.basename(subFile).replace(/\.(yaml|yml|md)$/, '');
163
+ const subKey = `${path.dirname(subFile)}/${subBaseName}`;
164
+ if (!fileMap.has(subKey)) {
165
+ fileMap.set(subKey, {});
166
+ }
167
+ if (subFile.endsWith('.yaml') || subFile.endsWith('.yml')) {
168
+ fileMap.get(subKey).yaml = subFile;
169
+ } else if (subFile.endsWith('.md')) {
170
+ fileMap.get(subKey).md = subFile;
171
+ }
172
+ }
173
+ } else if (entry.isFile()) {
174
+ const isYaml = entry.name.endsWith('.yaml') || entry.name.endsWith('.yml');
175
+ const isMd = entry.name.endsWith('.md');
176
+
177
+ if (isYaml || isMd) {
178
+ const baseName = entry.name.replace(/\.(yaml|yml|md)$/, '');
179
+ const key = `${categoryPath}/${baseName}`;
180
+
181
+ if (!fileMap.has(key)) {
182
+ fileMap.set(key, {});
183
+ }
184
+
185
+ if (isYaml) {
186
+ fileMap.get(key).yaml = fullPath;
187
+ } else {
188
+ fileMap.get(key).md = fullPath;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Build final list, preferring YAML over markdown
195
+ const files = [];
196
+ for (const paths of fileMap.values()) {
197
+ if (paths.yaml) {
198
+ files.push(paths.yaml);
199
+ } else if (paths.md) {
200
+ files.push(paths.md);
105
201
  }
106
202
  }
107
203
 
@@ -116,9 +212,164 @@ class StandardsIngestion {
116
212
  }
117
213
 
118
214
  /**
119
- * Parse a markdown standards file into Rapport pattern format
215
+ * Parse a standards file into Rapport pattern format
216
+ * Detects file type and routes to appropriate parser
120
217
  */
121
218
  async parseStandard(filepath, category) {
219
+ const isYaml = filepath.endsWith('.yaml') || filepath.endsWith('.yml');
220
+
221
+ if (isYaml) {
222
+ return this.parseYamlStandard(filepath, category);
223
+ }
224
+
225
+ return this.parseMarkdownStandard(filepath, category);
226
+ }
227
+
228
+ /**
229
+ * Parse a YAML standards file into Rapport pattern format
230
+ * YAML format:
231
+ * id: string
232
+ * category: string
233
+ * priority: 10 | 20 | 30
234
+ * rules:
235
+ * - action: ALWAYS | NEVER | USE | PREFER | AVOID
236
+ * rule: string
237
+ * applies_to: [glob patterns]
238
+ * anti_patterns:
239
+ * - string
240
+ */
241
+ async parseYamlStandard(filepath, category) {
242
+ const content = await fs.readFile(filepath, 'utf-8');
243
+ const ext = filepath.endsWith('.yml') ? '.yml' : '.yaml';
244
+ const filename = path.basename(filepath, ext);
245
+
246
+ let doc;
247
+ try {
248
+ doc = yaml.load(content);
249
+ } catch (parseError) {
250
+ throw new Error(`YAML parse error: ${parseError.message}`);
251
+ }
252
+
253
+ if (!doc || typeof doc !== 'object') {
254
+ throw new Error('Invalid YAML structure: expected an object');
255
+ }
256
+
257
+ const patterns = [];
258
+ const standardId = doc.id || filename;
259
+ const standardCategory = doc.category || category;
260
+ const standardPriority = doc.priority || 30;
261
+
262
+ // Extract anti-patterns as simple strings
263
+ const antiPatterns = Array.isArray(doc.anti_patterns)
264
+ ? doc.anti_patterns.map(ap => ({
265
+ description: typeof ap === 'string' ? ap : ap.description || 'Anti-pattern to avoid',
266
+ code: typeof ap === 'object' ? ap.code : null,
267
+ language: typeof ap === 'object' ? ap.language : null
268
+ }))
269
+ : [];
270
+
271
+ // Extract examples - now a named object in YAML format
272
+ const examples = [];
273
+ if (doc.examples && typeof doc.examples === 'object') {
274
+ for (const [name, code] of Object.entries(doc.examples)) {
275
+ examples.push({
276
+ description: name.replace(/_/g, ' '),
277
+ code: typeof code === 'string' ? code.trim() : String(code),
278
+ language: this.inferLanguageFromCode(code)
279
+ });
280
+ }
281
+ }
282
+
283
+ // Extract cost_impact - now a structured object in YAML format
284
+ const costImpact = doc.cost_impact && typeof doc.cost_impact === 'object'
285
+ ? doc.cost_impact
286
+ : (typeof doc.cost_impact === 'string' ? { description: doc.cost_impact } : null);
287
+
288
+ // Extract tags for metadata
289
+ const tags = Array.isArray(doc.tags) ? doc.tags : [];
290
+
291
+ // Process each rule
292
+ const rules = Array.isArray(doc.rules) ? doc.rules : [];
293
+
294
+ for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
295
+ const rule = rules[ruleIndex];
296
+ if (!rule || typeof rule !== 'object') continue;
297
+
298
+ const action = rule.action || 'USE';
299
+ const ruleText = rule.rule || '';
300
+ const appliesTo = Array.isArray(rule.applies_to) ? rule.applies_to : [];
301
+
302
+ // Construct element name from action and first few words
303
+ const element = this.extractElement(`${action} ${ruleText}`);
304
+
305
+ // Construct full rule statement
306
+ const fullRule = `${action}: ${ruleText}`;
307
+
308
+ const patternId = this.generatePatternId(standardCategory, standardId, element, ruleIndex);
309
+
310
+ // Map action to maturity
311
+ const actionMaturityMap = {
312
+ 'ALWAYS': 'enforced',
313
+ 'NEVER': 'enforced',
314
+ 'USE': 'validated',
315
+ 'PREFER': 'validated',
316
+ 'AVOID': 'validated'
317
+ };
318
+ const maturity = actionMaturityMap[action] || 'validated';
319
+
320
+ patterns.push({
321
+ pattern_id: patternId,
322
+ file_name: filename,
323
+ element: element,
324
+ rule: fullRule,
325
+ priority: standardPriority,
326
+ correlation: 1.0,
327
+ source: 'equilateral-standards',
328
+ maturity: maturity,
329
+ scope: 'organization',
330
+ category: standardCategory,
331
+ applicable_files: appliesTo.length > 0 ? appliesTo : this.extractApplicableFiles('', standardCategory),
332
+ anti_patterns: antiPatterns,
333
+ examples: examples,
334
+ cost_impact: costImpact,
335
+ tags: tags,
336
+ source_file: filepath,
337
+ title: standardId
338
+ });
339
+ }
340
+
341
+ // If no rules found but document exists, create a single pattern from the document
342
+ if (patterns.length === 0 && doc.id) {
343
+ const patternId = this.generatePatternId(standardCategory, standardId, 'standard_pattern');
344
+
345
+ patterns.push({
346
+ pattern_id: patternId,
347
+ file_name: filename,
348
+ element: 'Standard Pattern',
349
+ rule: `Standard: ${standardId}`,
350
+ priority: standardPriority,
351
+ correlation: 1.0,
352
+ source: 'equilateral-standards',
353
+ maturity: 'validated',
354
+ scope: 'organization',
355
+ category: standardCategory,
356
+ applicable_files: this.extractApplicableFiles('', standardCategory),
357
+ anti_patterns: antiPatterns,
358
+ examples: examples,
359
+ cost_impact: costImpact,
360
+ tags: tags,
361
+ source_file: filepath,
362
+ title: standardId
363
+ });
364
+ }
365
+
366
+ return { patterns };
367
+ }
368
+
369
+ /**
370
+ * Parse a markdown standards file into Rapport pattern format
371
+ */
372
+ async parseMarkdownStandard(filepath, category) {
122
373
  const content = await fs.readFile(filepath, 'utf-8');
123
374
  const filename = path.basename(filepath, '.md');
124
375
 
@@ -136,10 +387,21 @@ class StandardsIngestion {
136
387
  for (const rule of enforcementRules) {
137
388
  const patternId = this.generatePatternId(category, filename, rule.element);
138
389
 
390
+ // Map severity to priority (10=high, 20=medium, 30=low per DB constraint)
391
+ const priorityMap = {
392
+ 'CRITICAL': 10,
393
+ 'MANDATORY': 10,
394
+ 'ENFORCED': 20,
395
+ 'VALIDATED': 30
396
+ };
397
+ const priority = priorityMap[rule.severity] || 30;
398
+
139
399
  patterns.push({
140
400
  pattern_id: patternId,
401
+ file_name: filename, // Source markdown file name (without .md)
141
402
  element: rule.element,
142
403
  rule: rule.rule,
404
+ priority: priority,
143
405
  correlation: 1.0, // Standards are mandatory
144
406
  source: 'equilateral-standards',
145
407
  maturity: rule.severity === 'MANDATORY' || rule.severity === 'CRITICAL' ? 'enforced' : 'validated',
@@ -417,28 +679,63 @@ class StandardsIngestion {
417
679
  return blocks;
418
680
  }
419
681
 
682
+ /**
683
+ * Infer programming language from code content
684
+ */
685
+ inferLanguageFromCode(code) {
686
+ if (typeof code !== 'string') return 'text';
687
+
688
+ // YAML/SAM template patterns
689
+ if (code.includes('{{resolve:ssm:') || code.match(/^\s*(Environment|Resources|Properties):/m)) {
690
+ return 'yaml';
691
+ }
692
+ // JavaScript patterns
693
+ if (code.includes('async ') || code.includes('await ') || code.includes('const ') ||
694
+ code.includes('function ') || code.includes('=>') || code.includes('require(')) {
695
+ return 'javascript';
696
+ }
697
+ // SQL patterns
698
+ if (code.match(/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER)\b/i)) {
699
+ return 'sql';
700
+ }
701
+ // Python patterns
702
+ if (code.includes('def ') || code.includes('import ') || code.match(/^\s*class\s+\w+:/m)) {
703
+ return 'python';
704
+ }
705
+
706
+ return 'text';
707
+ }
708
+
420
709
  /**
421
710
  * Extract element name from a rule statement
711
+ * Goal: Create a descriptive, unique element name from the rule
422
712
  */
423
713
  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];
714
+ // Remove action prefix (ALWAYS, NEVER, USE, etc.)
715
+ let cleaned = statement.replace(/^(ALWAYS|NEVER|USE|PREFER|AVOID|DO NOT|DON'T)[:\s]+/i, '').trim();
716
+
717
+ // Extract key technical terms for better element names
718
+ // Pattern: "verb + object" -> extract the object/concept being acted upon
719
+ // Allow method calls like "client.end()" by not stopping at dots within identifiers
720
+ const verbObjectMatch = cleaned.match(/^(?:Use|Implement|Apply|Follow|Add|Create|Set|Cache|Validate|Configure|Enable|Disable|Fetch|Call)\s+(.+?)(?:\s+-\s+|,|$)/i);
721
+ if (verbObjectMatch) {
722
+ let object = verbObjectMatch[1].trim();
723
+ // Clean up trailing punctuation but keep method calls
724
+ object = object.replace(/\s*[-–].*$/, ''); // Remove explanatory text after dash
725
+ return object.substring(0, 50);
431
726
  }
432
727
 
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();
728
+ // For statements starting with a noun phrase, extract more context
729
+ const nounPhraseMatch = cleaned.match(/^([A-Za-z][a-z]+(?:[\s.][a-z()]+){0,5})/i);
730
+ if (nounPhraseMatch) {
731
+ let phrase = nounPhraseMatch[1].trim();
732
+ phrase = phrase.replace(/\s*[-–].*$/, ''); // Remove explanatory text
733
+ return phrase.substring(0, 50);
437
734
  }
438
735
 
439
- // Fallback: first few words
440
- const words = cleaned.split(/\s+/).slice(0, 3);
441
- return words.join(' ');
736
+ // Fallback: first meaningful words
737
+ const words = cleaned.split(/\s+/).slice(0, 4);
738
+ return words.join(' ').substring(0, 50);
442
739
  }
443
740
 
444
741
  /**
@@ -457,13 +754,20 @@ class StandardsIngestion {
457
754
 
458
755
  /**
459
756
  * Generate unique pattern ID
757
+ * @param {string} category - Category name
758
+ * @param {string} filename - Source filename
759
+ * @param {string} element - Element description
760
+ * @param {number} index - Rule index for uniqueness
460
761
  */
461
- generatePatternId(category, filename, element) {
762
+ generatePatternId(category, filename, element, index = 0) {
462
763
  const categorySlug = category.replace(/[^a-z0-9]/gi, '_');
463
764
  const fileSlug = filename.replace(/[^a-z0-9]/gi, '_');
464
- const elementSlug = element.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 30);
765
+ const elementSlug = element.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 40);
766
+
767
+ // Include index for uniqueness when elements might collide
768
+ const suffix = index > 0 ? `_${index}` : '';
465
769
 
466
- return `${categorySlug}_${fileSlug}_${elementSlug}`.toLowerCase();
770
+ return `${categorySlug}_${fileSlug}_${elementSlug}${suffix}`.toLowerCase();
467
771
  }
468
772
 
469
773
  /**
@@ -484,8 +788,10 @@ class StandardsIngestion {
484
788
  await executeQuery(`
485
789
  INSERT INTO rapport.standards_patterns (
486
790
  pattern_id,
791
+ file_name,
487
792
  element,
488
793
  rule,
794
+ priority,
489
795
  correlation,
490
796
  source,
491
797
  maturity,
@@ -495,13 +801,16 @@ class StandardsIngestion {
495
801
  anti_patterns,
496
802
  examples,
497
803
  cost_impact,
804
+ keywords,
498
805
  created_at,
499
806
  last_updated
500
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
807
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())
501
808
  ON CONFLICT (pattern_id)
502
809
  DO UPDATE SET
810
+ file_name = EXCLUDED.file_name,
503
811
  element = EXCLUDED.element,
504
812
  rule = EXCLUDED.rule,
813
+ priority = EXCLUDED.priority,
505
814
  correlation = EXCLUDED.correlation,
506
815
  maturity = EXCLUDED.maturity,
507
816
  scope = EXCLUDED.scope,
@@ -510,11 +819,14 @@ class StandardsIngestion {
510
819
  anti_patterns = EXCLUDED.anti_patterns,
511
820
  examples = EXCLUDED.examples,
512
821
  cost_impact = EXCLUDED.cost_impact,
822
+ keywords = EXCLUDED.keywords,
513
823
  last_updated = NOW()
514
824
  `, [
515
825
  pattern.pattern_id,
826
+ pattern.file_name,
516
827
  pattern.element,
517
828
  pattern.rule,
829
+ pattern.priority,
518
830
  pattern.correlation,
519
831
  pattern.source,
520
832
  pattern.maturity,
@@ -523,7 +835,8 @@ class StandardsIngestion {
523
835
  pattern.applicable_files,
524
836
  JSON.stringify(pattern.anti_patterns),
525
837
  JSON.stringify(pattern.examples),
526
- pattern.cost_impact ? JSON.stringify(pattern.cost_impact) : null
838
+ pattern.cost_impact ? JSON.stringify(pattern.cost_impact) : null,
839
+ pattern.tags || []
527
840
  ]);
528
841
 
529
842
  successCount++;