@equilateral_ai/mindmeld 3.2.0 → 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 (68) hide show
  1. package/README.md +4 -4
  2. package/hooks/README.md +46 -4
  3. package/hooks/pre-compact.js +87 -1
  4. package/hooks/session-end.js +292 -0
  5. package/hooks/session-start.js +292 -23
  6. package/package.json +4 -2
  7. package/scripts/auth-login.js +53 -0
  8. package/scripts/init-project.js +69 -375
  9. package/src/core/AuthManager.js +498 -0
  10. package/src/core/CrossReferenceEngine.js +624 -0
  11. package/src/core/DeprecationScheduler.js +183 -0
  12. package/src/core/LLMPatternDetector.js +218 -0
  13. package/src/core/RapportOrchestrator.js +186 -0
  14. package/src/core/RelevanceDetector.js +32 -2
  15. package/src/core/StandardLifecycle.js +244 -0
  16. package/src/core/StandardsIngestion.js +341 -28
  17. package/src/core/parsers/adrParser.js +479 -0
  18. package/src/core/parsers/cursorRulesParser.js +564 -0
  19. package/src/core/parsers/eslintParser.js +439 -0
  20. package/src/handlers/alerts/alertsAcknowledge.js +4 -3
  21. package/src/handlers/analytics/activitySummaryGet.js +235 -0
  22. package/src/handlers/analytics/coachingGet.js +361 -0
  23. package/src/handlers/analytics/developerScoreGet.js +207 -0
  24. package/src/handlers/collaborators/collaboratorAdd.js +4 -5
  25. package/src/handlers/collaborators/collaboratorInvite.js +6 -5
  26. package/src/handlers/collaborators/collaboratorList.js +3 -3
  27. package/src/handlers/collaborators/collaboratorRemove.js +5 -4
  28. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
  29. package/src/handlers/correlations/correlationsGet.js +1 -1
  30. package/src/handlers/correlations/correlationsProjectGet.js +7 -6
  31. package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
  32. package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
  33. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
  34. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
  35. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
  36. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
  37. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
  38. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
  39. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
  40. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
  41. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
  42. package/src/handlers/github/githubConnectionStatus.js +1 -1
  43. package/src/handlers/github/githubDiscoverPatterns.js +264 -5
  44. package/src/handlers/github/githubOAuthCallback.js +14 -2
  45. package/src/handlers/github/githubOAuthStart.js +1 -1
  46. package/src/handlers/github/githubPatternsReview.js +1 -1
  47. package/src/handlers/github/githubReposList.js +1 -1
  48. package/src/handlers/helpers/auditLogger.js +201 -0
  49. package/src/handlers/helpers/index.js +19 -1
  50. package/src/handlers/helpers/lambdaWrapper.js +1 -1
  51. package/src/handlers/notifications/sendNotification.js +1 -1
  52. package/src/handlers/projects/projectCreate.js +28 -1
  53. package/src/handlers/projects/projectDelete.js +3 -3
  54. package/src/handlers/projects/projectUpdate.js +4 -5
  55. package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
  56. package/src/handlers/scheduled/generateAlerts.js +1 -1
  57. package/src/handlers/standards/catalogGet.js +185 -0
  58. package/src/handlers/standards/catalogSync.js +120 -0
  59. package/src/handlers/standards/projectStandardsGet.js +135 -0
  60. package/src/handlers/standards/projectStandardsPut.js +131 -0
  61. package/src/handlers/standards/standardsAuditGet.js +65 -0
  62. package/src/handlers/standards/standardsParseUpload.js +153 -0
  63. package/src/handlers/standards/standardsRelevantPost.js +213 -0
  64. package/src/handlers/standards/standardsTransition.js +64 -0
  65. package/src/handlers/user/userSplashAck.js +91 -0
  66. package/src/handlers/user/userSplashGet.js +194 -0
  67. package/src/handlers/users/userProfilePut.js +77 -0
  68. package/src/index.js +37 -29
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Cursor Rules Parser - Parse AI coding assistant rule files into YAML standards format
3
+ *
4
+ * Supports:
5
+ * - .cursorrules (Cursor IDE)
6
+ * - CLAUDE.md (Claude Code)
7
+ * - .windsurfrules (Windsurf IDE)
8
+ * - Generic markdown rule files
9
+ *
10
+ * Extracts:
11
+ * - Rules from sections with action headers (always, never, avoid, prefer, use)
12
+ * - Banned patterns from "banned" or prohibition sections
13
+ * - Implementation patterns from code blocks
14
+ * - Trigger words and conventions
15
+ *
16
+ * Returns YAML-compatible object matching equilateral-standards schema
17
+ *
18
+ * @module parsers/cursorRulesParser
19
+ */
20
+
21
+ /**
22
+ * Section header patterns that indicate rule types
23
+ */
24
+ const SECTION_PATTERNS = {
25
+ ALWAYS: /\b(always|must|required|mandatory|enforce|critical)\b/i,
26
+ NEVER: /\b(never|banned|prohibited|forbidden|do not|don't)\b/i,
27
+ AVOID: /\b(avoid|discourage|anti-pattern|bad practice|don't use)\b/i,
28
+ PREFER: /\b(prefer|should|recommend|favor|better|best practice)\b/i,
29
+ USE: /\b(use|adopt|follow|implement|pattern|convention|standard)\b/i
30
+ };
31
+
32
+ /**
33
+ * Parse AI coding assistant rules file into YAML-compatible standards object
34
+ *
35
+ * @param {string} content - Raw markdown/text content of the rules file
36
+ * @param {Object} options - Parser options
37
+ * @param {string} options.filename - Source filename (e.g., '.cursorrules', 'CLAUDE.md')
38
+ * @param {string} options.category - Standards category override
39
+ * @returns {Object} YAML-compatible standards object
40
+ */
41
+ function parseCursorRules(content, options = {}) {
42
+ if (!content || typeof content !== 'string') {
43
+ throw new Error('Rules file content is required and must be a string');
44
+ }
45
+
46
+ const filename = options.filename || 'rules';
47
+ const sourceFormat = detectSourceFormat(filename);
48
+
49
+ const sections = parseSections(content);
50
+ const rules = extractRules(sections, content);
51
+ const antiPatterns = extractAntiPatterns(sections, content);
52
+ const examples = extractCodeExamples(content);
53
+ const category = options.category || inferCategory(content);
54
+
55
+ const id = generateId(filename);
56
+
57
+ return {
58
+ id,
59
+ category,
60
+ priority: determinePriority(rules),
61
+ rules,
62
+ anti_patterns: antiPatterns,
63
+ context: {
64
+ source_format: sourceFormat,
65
+ source_file: filename,
66
+ examples,
67
+ description: extractDescription(content)
68
+ },
69
+ tags: extractTags(content, sourceFormat),
70
+ updated: new Date().toISOString().split('T')[0]
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Detect the source format from filename
76
+ *
77
+ * @param {string} filename - Source filename
78
+ * @returns {string} Detected format identifier
79
+ */
80
+ function detectSourceFormat(filename) {
81
+ const lower = filename.toLowerCase();
82
+ if (lower.includes('.cursorrules') || lower.includes('cursorrules')) return 'cursorrules';
83
+ if (lower.includes('claude.md') || lower.includes('claude')) return 'claude-md';
84
+ if (lower.includes('.windsurfrules') || lower.includes('windsurfrules')) return 'windsurfrules';
85
+ return 'markdown';
86
+ }
87
+
88
+ /**
89
+ * Parse content into structured sections based on headers
90
+ *
91
+ * @param {string} content - Markdown content
92
+ * @returns {Array<Object>} Array of section objects with heading, level, body, and action
93
+ */
94
+ function parseSections(content) {
95
+ const sections = [];
96
+ const lines = content.split('\n');
97
+
98
+ let currentSection = null;
99
+ let bodyLines = [];
100
+
101
+ for (const line of lines) {
102
+ const headerMatch = line.match(/^(#{1,4})\s+(.+)$/);
103
+
104
+ if (headerMatch) {
105
+ // Save previous section
106
+ if (currentSection) {
107
+ currentSection.body = bodyLines.join('\n').trim();
108
+ sections.push(currentSection);
109
+ }
110
+
111
+ const heading = headerMatch[2].trim();
112
+ const level = headerMatch[1].length;
113
+ const action = classifySectionAction(heading);
114
+
115
+ currentSection = { heading, level, action, body: '' };
116
+ bodyLines = [];
117
+ } else {
118
+ bodyLines.push(line);
119
+ }
120
+ }
121
+
122
+ // Save final section
123
+ if (currentSection) {
124
+ currentSection.body = bodyLines.join('\n').trim();
125
+ sections.push(currentSection);
126
+ }
127
+
128
+ return sections;
129
+ }
130
+
131
+ /**
132
+ * Classify the action type for a section based on its heading
133
+ *
134
+ * @param {string} heading - Section heading text
135
+ * @returns {string|null} Action type or null if not a rule section
136
+ */
137
+ function classifySectionAction(heading) {
138
+ // Check NEVER before ALWAYS since some headings might match both
139
+ if (SECTION_PATTERNS.NEVER.test(heading)) return 'NEVER';
140
+ if (SECTION_PATTERNS.AVOID.test(heading)) return 'AVOID';
141
+ if (SECTION_PATTERNS.ALWAYS.test(heading)) return 'ALWAYS';
142
+ if (SECTION_PATTERNS.PREFER.test(heading)) return 'PREFER';
143
+ if (SECTION_PATTERNS.USE.test(heading)) return 'USE';
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Extract rules from parsed sections and raw content
149
+ *
150
+ * @param {Array<Object>} sections - Parsed sections
151
+ * @param {string} content - Raw content for fallback extraction
152
+ * @returns {Array<Object>} Array of rule objects with action and rule text
153
+ */
154
+ function extractRules(sections, content) {
155
+ const rules = [];
156
+ const seenRules = new Set();
157
+
158
+ // Extract rules from classified sections
159
+ for (const section of sections) {
160
+ if (!section.action) continue;
161
+
162
+ const sectionRules = extractRulesFromBody(section.body, section.action);
163
+ for (const rule of sectionRules) {
164
+ const key = normalizeForDedup(rule.action, rule.rule);
165
+ if (!seenRules.has(key)) {
166
+ seenRules.add(key);
167
+ rules.push(rule);
168
+ }
169
+ }
170
+ }
171
+
172
+ // Extract inline rules from content (lines with action keywords)
173
+ const inlineRules = extractInlineRules(content);
174
+ for (const rule of inlineRules) {
175
+ const key = normalizeForDedup(rule.action, rule.rule);
176
+ if (!seenRules.has(key)) {
177
+ seenRules.add(key);
178
+ rules.push(rule);
179
+ }
180
+ }
181
+
182
+ return rules;
183
+ }
184
+
185
+ /**
186
+ * Extract rules from a section body
187
+ *
188
+ * @param {string} body - Section body text
189
+ * @param {string} defaultAction - Default action for rules in this section
190
+ * @returns {Array<Object>} Array of rule objects
191
+ */
192
+ function extractRulesFromBody(body, defaultAction) {
193
+ if (!body) return [];
194
+
195
+ const rules = [];
196
+ const lines = body.split('\n');
197
+
198
+ for (const line of lines) {
199
+ const trimmed = line.trim();
200
+
201
+ // Skip empty lines, code block markers, and short lines
202
+ if (!trimmed || trimmed.startsWith('```') || trimmed.length < 10) continue;
203
+
204
+ // Skip lines that are inside code blocks
205
+ // (handled at a higher level by code block extraction)
206
+
207
+ // Extract list items
208
+ const listMatch = trimmed.match(/^[-*+]\s+(.+)$/);
209
+ if (listMatch) {
210
+ const ruleText = cleanRuleText(listMatch[1]);
211
+ if (ruleText.length >= 10) {
212
+ const action = detectInlineAction(ruleText) || defaultAction;
213
+ rules.push({
214
+ action,
215
+ rule: ruleText
216
+ });
217
+ }
218
+ continue;
219
+ }
220
+
221
+ // Extract numbered list items
222
+ const numberedMatch = trimmed.match(/^\d+\.?\s+(.+)$/);
223
+ if (numberedMatch) {
224
+ const ruleText = cleanRuleText(numberedMatch[1]);
225
+ if (ruleText.length >= 10) {
226
+ const action = detectInlineAction(ruleText) || defaultAction;
227
+ rules.push({
228
+ action,
229
+ rule: ruleText
230
+ });
231
+ }
232
+ continue;
233
+ }
234
+
235
+ // Extract bold statements as rules
236
+ const boldMatch = trimmed.match(/^\*\*(.+?)\*\*[:\s]*(.*)/);
237
+ if (boldMatch) {
238
+ const ruleText = cleanRuleText(`${boldMatch[1]}${boldMatch[2] ? ': ' + boldMatch[2] : ''}`);
239
+ if (ruleText.length >= 10) {
240
+ const action = detectInlineAction(ruleText) || defaultAction;
241
+ rules.push({
242
+ action,
243
+ rule: ruleText
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ return rules;
250
+ }
251
+
252
+ /**
253
+ * Extract rules that appear inline in content (not in classified sections)
254
+ * Looks for lines starting with action keywords
255
+ *
256
+ * @param {string} content - Raw content
257
+ * @returns {Array<Object>} Array of rule objects
258
+ */
259
+ function extractInlineRules(content) {
260
+ const rules = [];
261
+ const lines = content.split('\n');
262
+
263
+ let inCodeBlock = false;
264
+
265
+ for (const line of lines) {
266
+ const trimmed = line.trim();
267
+
268
+ // Track code blocks
269
+ if (trimmed.startsWith('```')) {
270
+ inCodeBlock = !inCodeBlock;
271
+ continue;
272
+ }
273
+ if (inCodeBlock) continue;
274
+
275
+ // Match lines that start with action keywords
276
+ const actionMatch = trimmed.match(
277
+ /^[-*]?\s*\*?\*?(ALWAYS|NEVER|AVOID|PREFER|USE|DO NOT|DON'T)\*?\*?[:\s]+(.+)/i
278
+ );
279
+
280
+ if (actionMatch) {
281
+ const keyword = actionMatch[1].toUpperCase();
282
+ const ruleText = cleanRuleText(actionMatch[2]);
283
+
284
+ if (ruleText.length >= 10) {
285
+ let action;
286
+ if (keyword === 'ALWAYS') action = 'ALWAYS';
287
+ else if (keyword === 'NEVER' || keyword === 'DO NOT' || keyword === "DON'T") action = 'NEVER';
288
+ else if (keyword === 'AVOID') action = 'AVOID';
289
+ else if (keyword === 'PREFER') action = 'PREFER';
290
+ else action = 'USE';
291
+
292
+ rules.push({ action, rule: ruleText });
293
+ }
294
+ }
295
+ }
296
+
297
+ return rules;
298
+ }
299
+
300
+ /**
301
+ * Normalize a rule for deduplication by stripping action prefixes and lowercasing
302
+ *
303
+ * @param {string} action - Rule action type
304
+ * @param {string} ruleText - Rule text
305
+ * @returns {string} Normalized deduplication key
306
+ */
307
+ function normalizeForDedup(action, ruleText) {
308
+ const normalized = ruleText
309
+ .toLowerCase()
310
+ .replace(/^(always|never|avoid|prefer|use|do not|don't|must not|shall not|must|shall|should)\s+/i, '')
311
+ .replace(/^(use|using|adopt|follow|implement)\s+/i, '')
312
+ .replace(/\s+/g, ' ')
313
+ .trim();
314
+ return `${action}:${normalized}`;
315
+ }
316
+
317
+ /**
318
+ * Detect if a rule text contains an inline action keyword
319
+ *
320
+ * @param {string} text - Rule text
321
+ * @returns {string|null} Detected action or null
322
+ */
323
+ function detectInlineAction(text) {
324
+ if (/^(?:never|do not|don't|must not|shall not)\b/i.test(text)) return 'NEVER';
325
+ if (/^(?:always|must|shall|required)\b/i.test(text)) return 'ALWAYS';
326
+ if (/^(?:avoid|discourage)\b/i.test(text)) return 'AVOID';
327
+ if (/^(?:prefer|should|recommend)\b/i.test(text)) return 'PREFER';
328
+ if (/^(?:use|adopt|follow)\b/i.test(text)) return 'USE';
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Extract anti-patterns from sections and content
334
+ *
335
+ * @param {Array<Object>} sections - Parsed sections
336
+ * @param {string} content - Raw content
337
+ * @returns {Array<string>} Array of anti-pattern descriptions
338
+ */
339
+ function extractAntiPatterns(sections, content) {
340
+ const antiPatterns = [];
341
+ const seen = new Set();
342
+
343
+ // Extract from NEVER and AVOID sections
344
+ for (const section of sections) {
345
+ if (section.action !== 'NEVER' && section.action !== 'AVOID') continue;
346
+
347
+ const lines = section.body.split('\n');
348
+ for (const line of lines) {
349
+ const trimmed = line.trim();
350
+ const listMatch = trimmed.match(/^[-*+]\s+(.+)$/);
351
+ if (listMatch && listMatch[1].length >= 10) {
352
+ const text = cleanRuleText(listMatch[1]);
353
+ if (!seen.has(text)) {
354
+ seen.add(text);
355
+ antiPatterns.push(text);
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ // Extract banned patterns from content
362
+ const bannedSection = content.match(/(?:^|\n)##?\s*(?:Banned|Prohibited|Forbidden)[^\n]*\n([\s\S]*?)(?=\n##?\s|\n*$)/i);
363
+ if (bannedSection) {
364
+ const lines = bannedSection[1].split('\n');
365
+ for (const line of lines) {
366
+ const listMatch = line.trim().match(/^[-*+]\s+(.+)$/);
367
+ if (listMatch && listMatch[1].length >= 10) {
368
+ const text = cleanRuleText(listMatch[1]);
369
+ if (!seen.has(text)) {
370
+ seen.add(text);
371
+ antiPatterns.push(text);
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ return antiPatterns;
378
+ }
379
+
380
+ /**
381
+ * Extract code examples from markdown code blocks
382
+ *
383
+ * @param {string} content - Raw markdown content
384
+ * @returns {Array<Object>} Array of example objects with language, code, and context
385
+ */
386
+ function extractCodeExamples(content) {
387
+ const examples = [];
388
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
389
+
390
+ let match;
391
+ while ((match = codeBlockRegex.exec(content)) !== null) {
392
+ const language = match[1] || 'text';
393
+ const code = match[2].trim();
394
+
395
+ if (code.length < 5) continue;
396
+
397
+ // Get surrounding context (text before the code block)
398
+ const beforeBlock = content.substring(
399
+ Math.max(0, match.index - 200),
400
+ match.index
401
+ );
402
+ const contextMatch = beforeBlock.match(/([^\n]+)\n*$/);
403
+ const description = contextMatch ? contextMatch[1].trim() : '';
404
+
405
+ examples.push({
406
+ language,
407
+ code,
408
+ description: cleanRuleText(description) || 'Code example'
409
+ });
410
+ }
411
+
412
+ return examples;
413
+ }
414
+
415
+ /**
416
+ * Extract a brief description from the top of the content
417
+ *
418
+ * @param {string} content - Raw content
419
+ * @returns {string} Brief description
420
+ */
421
+ function extractDescription(content) {
422
+ const lines = content.split('\n');
423
+
424
+ for (const line of lines) {
425
+ const trimmed = line.trim();
426
+ // Skip headers, empty lines, and code markers
427
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('```')) continue;
428
+ // Skip list items
429
+ if (trimmed.match(/^[-*+\d]/)) continue;
430
+
431
+ if (trimmed.length >= 20) {
432
+ return trimmed.substring(0, 200);
433
+ }
434
+ }
435
+
436
+ return '';
437
+ }
438
+
439
+ /**
440
+ * Clean rule text by removing markdown formatting
441
+ *
442
+ * @param {string} text - Raw text
443
+ * @returns {string} Cleaned text
444
+ */
445
+ function cleanRuleText(text) {
446
+ return text
447
+ .replace(/\*\*/g, '') // Remove bold
448
+ .replace(/\*/g, '') // Remove italic
449
+ .replace(/`([^`]+)`/g, '$1') // Remove inline code markers
450
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Replace links with text
451
+ .replace(/\s+/g, ' ') // Normalize whitespace
452
+ .replace(/\s*\.$/, '') // Remove trailing period
453
+ .trim();
454
+ }
455
+
456
+ /**
457
+ * Infer standards category from content
458
+ *
459
+ * @param {string} content - File content
460
+ * @returns {string} Inferred category
461
+ */
462
+ function inferCategory(content) {
463
+ const lower = content.toLowerCase();
464
+
465
+ const categoryKeywords = {
466
+ 'serverless-saas-aws': ['lambda', 'aws', 'serverless', 'sam', 'cloudformation', 'api gateway'],
467
+ 'frontend-development': ['react', 'vue', 'angular', 'component', 'css', 'tsx', 'jsx', 'frontend', 'ui'],
468
+ 'database': ['database', 'sql', 'postgresql', 'schema', 'migration', 'query'],
469
+ 'backend': ['api', 'handler', 'endpoint', 'express', 'node.js', 'server'],
470
+ 'multi-agent-orchestration': ['agent', 'orchestration', 'llm', 'prompt', 'ai assistant'],
471
+ 'compliance-security': ['security', 'auth', 'encryption', 'compliance', 'vulnerability'],
472
+ 'testing': ['test', 'jest', 'mocha', 'coverage', 'assertion'],
473
+ 'deployment': ['deploy', 'ci/cd', 'pipeline', 'docker', 'kubernetes']
474
+ };
475
+
476
+ let bestCategory = 'general';
477
+ let bestScore = 0;
478
+
479
+ for (const [category, keywords] of Object.entries(categoryKeywords)) {
480
+ const score = keywords.filter(kw => lower.includes(kw)).length;
481
+ if (score > bestScore) {
482
+ bestScore = score;
483
+ bestCategory = category;
484
+ }
485
+ }
486
+
487
+ return bestCategory;
488
+ }
489
+
490
+ /**
491
+ * Generate standards ID from filename
492
+ *
493
+ * @param {string} filename - Source filename
494
+ * @returns {string} Generated ID
495
+ */
496
+ function generateId(filename) {
497
+ const slug = filename
498
+ .replace(/^\./g, '')
499
+ .replace(/\.(md|txt|json)$/i, '')
500
+ .replace(/[^a-z0-9]+/gi, '-')
501
+ .toLowerCase();
502
+
503
+ return `rules-${slug}`;
504
+ }
505
+
506
+ /**
507
+ * Determine priority based on rule distribution
508
+ *
509
+ * @param {Array<Object>} rules - Extracted rules
510
+ * @returns {number} Priority (10, 20, or 30)
511
+ */
512
+ function determinePriority(rules) {
513
+ if (rules.length === 0) return 30;
514
+
515
+ const enforcedCount = rules.filter(r =>
516
+ r.action === 'ALWAYS' || r.action === 'NEVER'
517
+ ).length;
518
+
519
+ if (enforcedCount / rules.length > 0.5) return 10;
520
+ if (enforcedCount > 0) return 20;
521
+ return 30;
522
+ }
523
+
524
+ /**
525
+ * Extract tags from content and source format
526
+ *
527
+ * @param {string} content - File content
528
+ * @param {string} sourceFormat - Detected source format
529
+ * @returns {Array<string>} Tags
530
+ */
531
+ function extractTags(content, sourceFormat) {
532
+ const tags = [sourceFormat, 'ai-rules'];
533
+
534
+ const lower = content.toLowerCase();
535
+
536
+ const tagKeywords = {
537
+ 'code-style': /\b(style|formatting|naming|convention)\b/i,
538
+ 'architecture': /\barchitectur/i,
539
+ 'security': /\bsecurity\b/i,
540
+ 'performance': /\bperformance\b/i,
541
+ 'testing': /\btesting\b/i,
542
+ 'documentation': /\bdocument/i,
543
+ 'error-handling': /\berror.?handling\b/i,
544
+ 'typescript': /\btypescript\b/i,
545
+ 'react': /\breact\b/i,
546
+ 'node': /\bnode\.?js\b/i
547
+ };
548
+
549
+ for (const [tag, pattern] of Object.entries(tagKeywords)) {
550
+ if (pattern.test(lower)) {
551
+ tags.push(tag);
552
+ }
553
+ }
554
+
555
+ return [...new Set(tags)];
556
+ }
557
+
558
+ module.exports = {
559
+ parseCursorRules,
560
+ parseSections,
561
+ extractRules,
562
+ extractAntiPatterns,
563
+ extractCodeExamples
564
+ };