@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.
- package/README.md +4 -4
- package/hooks/README.md +46 -4
- package/hooks/pre-compact.js +87 -1
- package/hooks/session-end.js +292 -0
- package/hooks/session-start.js +292 -23
- package/package.json +4 -2
- package/scripts/auth-login.js +53 -0
- package/scripts/init-project.js +69 -375
- package/src/core/AuthManager.js +498 -0
- package/src/core/CrossReferenceEngine.js +624 -0
- package/src/core/DeprecationScheduler.js +183 -0
- package/src/core/LLMPatternDetector.js +218 -0
- package/src/core/RapportOrchestrator.js +186 -0
- package/src/core/RelevanceDetector.js +32 -2
- package/src/core/StandardLifecycle.js +244 -0
- package/src/core/StandardsIngestion.js +341 -28
- package/src/core/parsers/adrParser.js +479 -0
- package/src/core/parsers/cursorRulesParser.js +564 -0
- package/src/core/parsers/eslintParser.js +439 -0
- package/src/handlers/alerts/alertsAcknowledge.js +4 -3
- package/src/handlers/analytics/activitySummaryGet.js +235 -0
- package/src/handlers/analytics/coachingGet.js +361 -0
- package/src/handlers/analytics/developerScoreGet.js +207 -0
- package/src/handlers/collaborators/collaboratorAdd.js +4 -5
- package/src/handlers/collaborators/collaboratorInvite.js +6 -5
- package/src/handlers/collaborators/collaboratorList.js +3 -3
- package/src/handlers/collaborators/collaboratorRemove.js +5 -4
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
- package/src/handlers/correlations/correlationsGet.js +1 -1
- package/src/handlers/correlations/correlationsProjectGet.js +7 -6
- package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
- package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
- package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
- package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
- package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
- package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
- package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
- package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
- package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +264 -5
- package/src/handlers/github/githubOAuthCallback.js +14 -2
- package/src/handlers/github/githubOAuthStart.js +1 -1
- package/src/handlers/github/githubPatternsReview.js +1 -1
- package/src/handlers/github/githubReposList.js +1 -1
- package/src/handlers/helpers/auditLogger.js +201 -0
- package/src/handlers/helpers/index.js +19 -1
- package/src/handlers/helpers/lambdaWrapper.js +1 -1
- package/src/handlers/notifications/sendNotification.js +1 -1
- package/src/handlers/projects/projectCreate.js +28 -1
- package/src/handlers/projects/projectDelete.js +3 -3
- package/src/handlers/projects/projectUpdate.js +4 -5
- package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
- package/src/handlers/scheduled/generateAlerts.js +1 -1
- package/src/handlers/standards/catalogGet.js +185 -0
- package/src/handlers/standards/catalogSync.js +120 -0
- package/src/handlers/standards/projectStandardsGet.js +135 -0
- package/src/handlers/standards/projectStandardsPut.js +131 -0
- package/src/handlers/standards/standardsAuditGet.js +65 -0
- package/src/handlers/standards/standardsParseUpload.js +153 -0
- package/src/handlers/standards/standardsRelevantPost.js +213 -0
- package/src/handlers/standards/standardsTransition.js +64 -0
- package/src/handlers/user/userSplashAck.js +91 -0
- package/src/handlers/user/userSplashGet.js +194 -0
- package/src/handlers/users/userProfilePut.js +77 -0
- 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
|
+
};
|