cursor-recursive-rag 0.2.0-alpha.2 → 0.2.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 (210) hide show
  1. package/README.md +179 -203
  2. package/dist/adapters/llm/anthropic.d.ts +27 -0
  3. package/dist/adapters/llm/anthropic.d.ts.map +1 -0
  4. package/dist/adapters/llm/anthropic.js +287 -0
  5. package/dist/adapters/llm/anthropic.js.map +1 -0
  6. package/dist/adapters/llm/base.d.ts +62 -0
  7. package/dist/adapters/llm/base.d.ts.map +1 -0
  8. package/dist/adapters/llm/base.js +140 -0
  9. package/dist/adapters/llm/base.js.map +1 -0
  10. package/dist/adapters/llm/deepseek.d.ts +24 -0
  11. package/dist/adapters/llm/deepseek.d.ts.map +1 -0
  12. package/dist/adapters/llm/deepseek.js +228 -0
  13. package/dist/adapters/llm/deepseek.js.map +1 -0
  14. package/dist/adapters/llm/groq.d.ts +25 -0
  15. package/dist/adapters/llm/groq.d.ts.map +1 -0
  16. package/dist/adapters/llm/groq.js +265 -0
  17. package/dist/adapters/llm/groq.js.map +1 -0
  18. package/dist/adapters/llm/index.d.ts +62 -0
  19. package/dist/adapters/llm/index.d.ts.map +1 -0
  20. package/dist/adapters/llm/index.js +380 -0
  21. package/dist/adapters/llm/index.js.map +1 -0
  22. package/dist/adapters/llm/ollama.d.ts +23 -0
  23. package/dist/adapters/llm/ollama.d.ts.map +1 -0
  24. package/dist/adapters/llm/ollama.js +261 -0
  25. package/dist/adapters/llm/ollama.js.map +1 -0
  26. package/dist/adapters/llm/openai.d.ts +22 -0
  27. package/dist/adapters/llm/openai.d.ts.map +1 -0
  28. package/dist/adapters/llm/openai.js +232 -0
  29. package/dist/adapters/llm/openai.js.map +1 -0
  30. package/dist/adapters/llm/openrouter.d.ts +27 -0
  31. package/dist/adapters/llm/openrouter.d.ts.map +1 -0
  32. package/dist/adapters/llm/openrouter.js +305 -0
  33. package/dist/adapters/llm/openrouter.js.map +1 -0
  34. package/dist/adapters/vector/index.d.ts.map +1 -1
  35. package/dist/adapters/vector/index.js +8 -0
  36. package/dist/adapters/vector/index.js.map +1 -1
  37. package/dist/adapters/vector/redis-native.d.ts +35 -0
  38. package/dist/adapters/vector/redis-native.d.ts.map +1 -0
  39. package/dist/adapters/vector/redis-native.js +170 -0
  40. package/dist/adapters/vector/redis-native.js.map +1 -0
  41. package/dist/cli/commands/chat.d.ts +4 -0
  42. package/dist/cli/commands/chat.d.ts.map +1 -0
  43. package/dist/cli/commands/chat.js +374 -0
  44. package/dist/cli/commands/chat.js.map +1 -0
  45. package/dist/cli/commands/maintenance.d.ts +4 -0
  46. package/dist/cli/commands/maintenance.d.ts.map +1 -0
  47. package/dist/cli/commands/maintenance.js +237 -0
  48. package/dist/cli/commands/maintenance.js.map +1 -0
  49. package/dist/cli/commands/rules.d.ts +9 -0
  50. package/dist/cli/commands/rules.d.ts.map +1 -0
  51. package/dist/cli/commands/rules.js +639 -0
  52. package/dist/cli/commands/rules.js.map +1 -0
  53. package/dist/cli/commands/setup.js +5 -4
  54. package/dist/cli/commands/setup.js.map +1 -1
  55. package/dist/cli/index.js +6 -0
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/config/memoryConfig.d.ts +427 -0
  58. package/dist/config/memoryConfig.d.ts.map +1 -0
  59. package/dist/config/memoryConfig.js +258 -0
  60. package/dist/config/memoryConfig.js.map +1 -0
  61. package/dist/config/rulesConfig.d.ts +486 -0
  62. package/dist/config/rulesConfig.d.ts.map +1 -0
  63. package/dist/config/rulesConfig.js +345 -0
  64. package/dist/config/rulesConfig.js.map +1 -0
  65. package/dist/dashboard/coreTools.d.ts +14 -0
  66. package/dist/dashboard/coreTools.d.ts.map +1 -0
  67. package/dist/dashboard/coreTools.js +413 -0
  68. package/dist/dashboard/coreTools.js.map +1 -0
  69. package/dist/dashboard/public/index.html +1982 -13
  70. package/dist/dashboard/server.d.ts +1 -8
  71. package/dist/dashboard/server.d.ts.map +1 -1
  72. package/dist/dashboard/server.js +846 -13
  73. package/dist/dashboard/server.js.map +1 -1
  74. package/dist/dashboard/toolRegistry.d.ts +192 -0
  75. package/dist/dashboard/toolRegistry.d.ts.map +1 -0
  76. package/dist/dashboard/toolRegistry.js +322 -0
  77. package/dist/dashboard/toolRegistry.js.map +1 -0
  78. package/dist/proxy/index.d.ts +1 -1
  79. package/dist/proxy/index.d.ts.map +1 -1
  80. package/dist/proxy/index.js +9 -6
  81. package/dist/proxy/index.js.map +1 -1
  82. package/dist/server/index.js +21 -0
  83. package/dist/server/index.js.map +1 -1
  84. package/dist/server/tools/crawl.d.ts.map +1 -1
  85. package/dist/server/tools/crawl.js +8 -0
  86. package/dist/server/tools/crawl.js.map +1 -1
  87. package/dist/server/tools/index.d.ts.map +1 -1
  88. package/dist/server/tools/index.js +19 -1
  89. package/dist/server/tools/index.js.map +1 -1
  90. package/dist/server/tools/ingest.d.ts.map +1 -1
  91. package/dist/server/tools/ingest.js +5 -0
  92. package/dist/server/tools/ingest.js.map +1 -1
  93. package/dist/server/tools/memory.d.ts +250 -0
  94. package/dist/server/tools/memory.d.ts.map +1 -0
  95. package/dist/server/tools/memory.js +472 -0
  96. package/dist/server/tools/memory.js.map +1 -0
  97. package/dist/server/tools/recursive-query.d.ts.map +1 -1
  98. package/dist/server/tools/recursive-query.js +6 -0
  99. package/dist/server/tools/recursive-query.js.map +1 -1
  100. package/dist/server/tools/search.d.ts.map +1 -1
  101. package/dist/server/tools/search.js +6 -0
  102. package/dist/server/tools/search.js.map +1 -1
  103. package/dist/services/activity-log.d.ts +10 -0
  104. package/dist/services/activity-log.d.ts.map +1 -0
  105. package/dist/services/activity-log.js +53 -0
  106. package/dist/services/activity-log.js.map +1 -0
  107. package/dist/services/categoryManager.d.ts +110 -0
  108. package/dist/services/categoryManager.d.ts.map +1 -0
  109. package/dist/services/categoryManager.js +549 -0
  110. package/dist/services/categoryManager.js.map +1 -0
  111. package/dist/services/contextEnvironment.d.ts +206 -0
  112. package/dist/services/contextEnvironment.d.ts.map +1 -0
  113. package/dist/services/contextEnvironment.js +481 -0
  114. package/dist/services/contextEnvironment.js.map +1 -0
  115. package/dist/services/conversationProcessor.d.ts +99 -0
  116. package/dist/services/conversationProcessor.d.ts.map +1 -0
  117. package/dist/services/conversationProcessor.js +311 -0
  118. package/dist/services/conversationProcessor.js.map +1 -0
  119. package/dist/services/cursorChatReader.d.ts +129 -0
  120. package/dist/services/cursorChatReader.d.ts.map +1 -0
  121. package/dist/services/cursorChatReader.js +419 -0
  122. package/dist/services/cursorChatReader.js.map +1 -0
  123. package/dist/services/decayCalculator.d.ts +85 -0
  124. package/dist/services/decayCalculator.d.ts.map +1 -0
  125. package/dist/services/decayCalculator.js +182 -0
  126. package/dist/services/decayCalculator.js.map +1 -0
  127. package/dist/services/enhancedVectorStore.d.ts +102 -0
  128. package/dist/services/enhancedVectorStore.d.ts.map +1 -0
  129. package/dist/services/enhancedVectorStore.js +245 -0
  130. package/dist/services/enhancedVectorStore.js.map +1 -0
  131. package/dist/services/hybridScorer.d.ts +120 -0
  132. package/dist/services/hybridScorer.d.ts.map +1 -0
  133. package/dist/services/hybridScorer.js +334 -0
  134. package/dist/services/hybridScorer.js.map +1 -0
  135. package/dist/services/knowledgeExtractor.d.ts +45 -0
  136. package/dist/services/knowledgeExtractor.d.ts.map +1 -0
  137. package/dist/services/knowledgeExtractor.js +436 -0
  138. package/dist/services/knowledgeExtractor.js.map +1 -0
  139. package/dist/services/knowledgeStorage.d.ts +102 -0
  140. package/dist/services/knowledgeStorage.d.ts.map +1 -0
  141. package/dist/services/knowledgeStorage.js +383 -0
  142. package/dist/services/knowledgeStorage.js.map +1 -0
  143. package/dist/services/maintenanceScheduler.d.ts +89 -0
  144. package/dist/services/maintenanceScheduler.d.ts.map +1 -0
  145. package/dist/services/maintenanceScheduler.js +479 -0
  146. package/dist/services/maintenanceScheduler.js.map +1 -0
  147. package/dist/services/memoryMetadataStore.d.ts +62 -0
  148. package/dist/services/memoryMetadataStore.d.ts.map +1 -0
  149. package/dist/services/memoryMetadataStore.js +570 -0
  150. package/dist/services/memoryMetadataStore.js.map +1 -0
  151. package/dist/services/recursiveRetrieval.d.ts +122 -0
  152. package/dist/services/recursiveRetrieval.d.ts.map +1 -0
  153. package/dist/services/recursiveRetrieval.js +443 -0
  154. package/dist/services/recursiveRetrieval.js.map +1 -0
  155. package/dist/services/relationshipGraph.d.ts +77 -0
  156. package/dist/services/relationshipGraph.d.ts.map +1 -0
  157. package/dist/services/relationshipGraph.js +411 -0
  158. package/dist/services/relationshipGraph.js.map +1 -0
  159. package/dist/services/rlmSafeguards.d.ts +273 -0
  160. package/dist/services/rlmSafeguards.d.ts.map +1 -0
  161. package/dist/services/rlmSafeguards.js +705 -0
  162. package/dist/services/rlmSafeguards.js.map +1 -0
  163. package/dist/services/rulesAnalyzer.d.ts +119 -0
  164. package/dist/services/rulesAnalyzer.d.ts.map +1 -0
  165. package/dist/services/rulesAnalyzer.js +768 -0
  166. package/dist/services/rulesAnalyzer.js.map +1 -0
  167. package/dist/services/rulesMerger.d.ts +75 -0
  168. package/dist/services/rulesMerger.d.ts.map +1 -0
  169. package/dist/services/rulesMerger.js +404 -0
  170. package/dist/services/rulesMerger.js.map +1 -0
  171. package/dist/services/rulesParser.d.ts +127 -0
  172. package/dist/services/rulesParser.d.ts.map +1 -0
  173. package/dist/services/rulesParser.js +594 -0
  174. package/dist/services/rulesParser.js.map +1 -0
  175. package/dist/services/smartChunker.d.ts +110 -0
  176. package/dist/services/smartChunker.d.ts.map +1 -0
  177. package/dist/services/smartChunker.js +520 -0
  178. package/dist/services/smartChunker.js.map +1 -0
  179. package/dist/types/categories.d.ts +105 -0
  180. package/dist/types/categories.d.ts.map +1 -0
  181. package/dist/types/categories.js +108 -0
  182. package/dist/types/categories.js.map +1 -0
  183. package/dist/types/extractedKnowledge.d.ts +233 -0
  184. package/dist/types/extractedKnowledge.d.ts.map +1 -0
  185. package/dist/types/extractedKnowledge.js +56 -0
  186. package/dist/types/extractedKnowledge.js.map +1 -0
  187. package/dist/types/index.d.ts +9 -2
  188. package/dist/types/index.d.ts.map +1 -1
  189. package/dist/types/index.js +12 -1
  190. package/dist/types/index.js.map +1 -1
  191. package/dist/types/llmProvider.d.ts +282 -0
  192. package/dist/types/llmProvider.d.ts.map +1 -0
  193. package/dist/types/llmProvider.js +48 -0
  194. package/dist/types/llmProvider.js.map +1 -0
  195. package/dist/types/memory.d.ts +227 -0
  196. package/dist/types/memory.d.ts.map +1 -0
  197. package/dist/types/memory.js +76 -0
  198. package/dist/types/memory.js.map +1 -0
  199. package/dist/types/relationships.d.ts +167 -0
  200. package/dist/types/relationships.d.ts.map +1 -0
  201. package/dist/types/relationships.js +106 -0
  202. package/dist/types/relationships.js.map +1 -0
  203. package/dist/types/rulesOptimizer.d.ts +345 -0
  204. package/dist/types/rulesOptimizer.d.ts.map +1 -0
  205. package/dist/types/rulesOptimizer.js +22 -0
  206. package/dist/types/rulesOptimizer.js.map +1 -0
  207. package/docs/cursor-recursive-rag-memory-spec.md +4569 -0
  208. package/docs/cursor-recursive-rag-tasks.md +1355 -0
  209. package/package.json +6 -3
  210. package/restart-rag.sh +16 -0
@@ -0,0 +1,768 @@
1
+ /**
2
+ * Rules Analyzer Service
3
+ *
4
+ * Detects duplicates, conflicts, and outdated patterns in Cursor rules.
5
+ * Uses embeddings for semantic similarity and optional LLM for deeper analysis.
6
+ */
7
+ import { createHash } from 'crypto';
8
+ import { createEmbedder } from '../adapters/embeddings/index.js';
9
+ import { getLLMProvider } from '../adapters/llm/index.js';
10
+ import { DEFAULT_OPTIMIZER_OPTIONS } from '../types/rulesOptimizer.js';
11
+ import { loadRulesConfig, } from '../config/rulesConfig.js';
12
+ /**
13
+ * Cosine similarity between two vectors
14
+ */
15
+ function cosineSimilarity(a, b) {
16
+ if (a.length !== b.length)
17
+ return 0;
18
+ let dotProduct = 0;
19
+ let normA = 0;
20
+ let normB = 0;
21
+ for (let i = 0; i < a.length; i++) {
22
+ dotProduct += (a[i] ?? 0) * (b[i] ?? 0);
23
+ normA += (a[i] ?? 0) * (a[i] ?? 0);
24
+ normB += (b[i] ?? 0) * (b[i] ?? 0);
25
+ }
26
+ if (normA === 0 || normB === 0)
27
+ return 0;
28
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
29
+ }
30
+ /**
31
+ * Jaccard similarity between two sets of tags
32
+ */
33
+ function jaccardSimilarity(set1, set2) {
34
+ const intersection = new Set([...set1].filter(x => set2.has(x)));
35
+ const union = new Set([...set1, ...set2]);
36
+ if (union.size === 0)
37
+ return 0;
38
+ return intersection.size / union.size;
39
+ }
40
+ /**
41
+ * Rules Analyzer for detecting duplicates, conflicts, and outdated patterns
42
+ */
43
+ export class RulesAnalyzer {
44
+ embeddings = null;
45
+ llm;
46
+ config;
47
+ options;
48
+ rulesConfig;
49
+ ruleEmbeddings = new Map();
50
+ constructor(config, options, rulesConfig) {
51
+ this.config = config;
52
+ this.rulesConfig = rulesConfig ?? loadRulesConfig();
53
+ this.llm = (options?.useLLM !== false && this.rulesConfig.analysis.useLLM)
54
+ ? getLLMProvider()
55
+ : null;
56
+ this.options = {
57
+ ...DEFAULT_OPTIMIZER_OPTIONS,
58
+ ...options,
59
+ duplicateThreshold: options?.duplicateThreshold ?? this.rulesConfig.analysis.duplicateThreshold,
60
+ detectConflicts: options?.detectConflicts ?? this.rulesConfig.analysis.detectConflicts,
61
+ detectOutdated: options?.detectOutdated ?? this.rulesConfig.analysis.detectOutdated,
62
+ };
63
+ }
64
+ /**
65
+ * Get the current rules configuration
66
+ */
67
+ getConfig() {
68
+ return this.rulesConfig;
69
+ }
70
+ /**
71
+ * Update rules configuration
72
+ */
73
+ setConfig(config) {
74
+ this.rulesConfig = config;
75
+ }
76
+ async getEmbeddings() {
77
+ if (!this.embeddings) {
78
+ this.embeddings = await createEmbedder(this.config.embeddings, this.config);
79
+ }
80
+ return this.embeddings;
81
+ }
82
+ /**
83
+ * Analyze rules and generate optimization report
84
+ */
85
+ async analyzeRules(rules, analyzedPath) {
86
+ const startTime = Date.now();
87
+ // Generate embeddings for all rules
88
+ await this.generateEmbeddings(rules);
89
+ // Find duplicates
90
+ const duplicates = await this.findDuplicates(rules);
91
+ // Find clusters
92
+ const clusters = this.findClusters(rules);
93
+ // Find conflicts
94
+ const conflicts = this.options.detectConflicts
95
+ ? await this.findConflicts(rules)
96
+ : [];
97
+ // Find outdated rules
98
+ const outdated = this.options.detectOutdated
99
+ ? this.findOutdatedRules(rules)
100
+ : [];
101
+ // Generate merge candidates
102
+ const merges = await this.generateMergeCandidates(clusters, duplicates);
103
+ // Calculate statistics
104
+ const totalTokensBefore = rules.reduce((sum, r) => sum + r.tokenCount, 0);
105
+ const estimatedSavings = this.estimateSavings(merges, duplicates);
106
+ const totalTokensAfter = totalTokensBefore - estimatedSavings;
107
+ // Build optimization plan
108
+ const plan = this.buildOptimizationPlan(duplicates, merges, conflicts, outdated);
109
+ // Build file changes
110
+ const fileChanges = this.buildFileChanges(plan, rules);
111
+ return {
112
+ generatedAt: new Date(),
113
+ analyzedPath,
114
+ summary: {
115
+ totalFiles: new Set(rules.map(r => r.sourceFile.path)).size,
116
+ totalRules: rules.length,
117
+ totalTokensBefore,
118
+ totalTokensAfter,
119
+ savingsPercent: totalTokensBefore > 0
120
+ ? Math.round((estimatedSavings / totalTokensBefore) * 100)
121
+ : 0,
122
+ duplicatesFound: duplicates.length,
123
+ conflictsFound: conflicts.length,
124
+ mergeCandidates: merges.length,
125
+ outdatedRules: outdated.length,
126
+ },
127
+ findings: {
128
+ duplicates,
129
+ clusters,
130
+ merges,
131
+ conflicts,
132
+ outdated,
133
+ },
134
+ plan,
135
+ fileChanges,
136
+ };
137
+ }
138
+ /**
139
+ * Generate embeddings for all rules
140
+ */
141
+ async generateEmbeddings(rules) {
142
+ const embedder = await this.getEmbeddings();
143
+ for (const rule of rules) {
144
+ if (!this.ruleEmbeddings.has(rule.id)) {
145
+ const embedding = await embedder.embed(`${rule.title}\n\n${rule.content.substring(0, 2000)}`);
146
+ this.ruleEmbeddings.set(rule.id, embedding);
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Find duplicate and near-duplicate rules
152
+ */
153
+ async findDuplicates(rules) {
154
+ const duplicates = [];
155
+ const seen = new Set();
156
+ for (let i = 0; i < rules.length; i++) {
157
+ for (let j = i + 1; j < rules.length; j++) {
158
+ const rule1 = rules[i];
159
+ const rule2 = rules[j];
160
+ const pairKey = [rule1.id, rule2.id].sort().join(':');
161
+ if (seen.has(pairKey))
162
+ continue;
163
+ seen.add(pairKey);
164
+ // Check for exact duplicates (content hash)
165
+ if (rule1.contentHash === rule2.contentHash) {
166
+ duplicates.push({
167
+ rule1,
168
+ rule2,
169
+ similarity: 1.0,
170
+ matchType: 'exact',
171
+ overlappingConcepts: ['Identical content'],
172
+ recommendation: 'keep_newer',
173
+ });
174
+ continue;
175
+ }
176
+ // Check semantic similarity via embeddings
177
+ const emb1 = this.ruleEmbeddings.get(rule1.id);
178
+ const emb2 = this.ruleEmbeddings.get(rule2.id);
179
+ if (emb1 && emb2) {
180
+ const similarity = cosineSimilarity(emb1, emb2);
181
+ if (similarity >= this.options.duplicateThreshold) {
182
+ const matchType = this.determineMatchType(rule1, rule2, similarity);
183
+ const overlappingConcepts = this.findOverlappingConcepts(rule1, rule2);
184
+ const recommendation = this.determineRecommendation(matchType, rule1, rule2);
185
+ duplicates.push({
186
+ rule1,
187
+ rule2,
188
+ similarity,
189
+ matchType,
190
+ overlappingConcepts,
191
+ recommendation,
192
+ });
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // Sort by similarity descending
198
+ return duplicates.sort((a, b) => b.similarity - a.similarity);
199
+ }
200
+ /**
201
+ * Find clusters of related rules
202
+ */
203
+ findClusters(rules) {
204
+ const clusters = [];
205
+ const clustered = new Set();
206
+ // Group by common tags first
207
+ const tagGroups = new Map();
208
+ for (const rule of rules) {
209
+ for (const tag of rule.tags) {
210
+ const group = tagGroups.get(tag) ?? [];
211
+ group.push(rule);
212
+ tagGroups.set(tag, group);
213
+ }
214
+ }
215
+ // Create clusters from tag groups with multiple rules
216
+ for (const [tag, groupRules] of tagGroups) {
217
+ if (groupRules.length < 2)
218
+ continue;
219
+ if (groupRules.every(r => clustered.has(r.id)))
220
+ continue;
221
+ // Check if rules are semantically related
222
+ const avgSimilarity = this.calculateAverageClusterSimilarity(groupRules);
223
+ if (avgSimilarity < 0.5)
224
+ continue;
225
+ const newRules = groupRules.filter(r => !clustered.has(r.id));
226
+ if (newRules.length < 2)
227
+ continue;
228
+ const totalTokensSeparate = newRules.reduce((sum, r) => sum + r.tokenCount, 0);
229
+ const estimatedTokensMerged = Math.ceil(totalTokensSeparate * 0.6); // Assume 40% reduction
230
+ clusters.push({
231
+ id: `cluster-${createHash('md5').update(tag).digest('hex').substring(0, 8)}`,
232
+ suggestedName: `${tag.charAt(0).toUpperCase() + tag.slice(1)} Rules`,
233
+ rules: newRules,
234
+ commonTags: [tag],
235
+ topic: tag,
236
+ confidence: avgSimilarity,
237
+ totalTokensSeparate,
238
+ estimatedTokensMerged,
239
+ savingsPercent: Math.round(((totalTokensSeparate - estimatedTokensMerged) / totalTokensSeparate) * 100),
240
+ });
241
+ newRules.forEach(r => clustered.add(r.id));
242
+ }
243
+ return clusters.sort((a, b) => b.savingsPercent - a.savingsPercent);
244
+ }
245
+ /**
246
+ * Find conflicts between rules
247
+ */
248
+ async findConflicts(rules) {
249
+ const conflicts = [];
250
+ // Common conflict patterns
251
+ const conflictPatterns = [
252
+ { pattern: /\b(always|never|must|must not|do not|don't)\b/gi, type: 'directive' },
253
+ { pattern: /\bprefer\s+(\w+)\b/gi, type: 'preference' },
254
+ { pattern: /\buse\s+(\w+)\s+instead\s+of\s+(\w+)/gi, type: 'replacement' },
255
+ { pattern: /\bv?(\d+\.?\d*\.?\d*)\b/g, type: 'version' },
256
+ ];
257
+ for (let i = 0; i < rules.length; i++) {
258
+ for (let j = i + 1; j < rules.length; j++) {
259
+ const rule1 = rules[i];
260
+ const rule2 = rules[j];
261
+ // Check for tag overlap (rules about same topic might conflict)
262
+ const tagOverlap = jaccardSimilarity(new Set(rule1.tags), new Set(rule2.tags));
263
+ if (tagOverlap < 0.3)
264
+ continue;
265
+ // Check for conflicting directives
266
+ const conflict = this.detectConflict(rule1, rule2, conflictPatterns);
267
+ if (conflict) {
268
+ conflicts.push(conflict);
269
+ }
270
+ }
271
+ }
272
+ return conflicts;
273
+ }
274
+ /**
275
+ * Find potentially outdated rules
276
+ *
277
+ * Uses configurable patterns from rulesConfig plus some default heuristics:
278
+ * - File age (configurable maxAgeDays)
279
+ * - Old year references (configurable oldYearThreshold)
280
+ * - User-defined version checks
281
+ * - User-defined deprecation patterns
282
+ */
283
+ findOutdatedRules(rules) {
284
+ const outdated = [];
285
+ const { maxAgeDays, oldYearThreshold } = this.rulesConfig.analysis;
286
+ const versionChecks = this.rulesConfig.versionChecks.filter(v => v.enabled);
287
+ const deprecationPatterns = this.rulesConfig.deprecationPatterns.filter(d => d.enabled);
288
+ for (const rule of rules) {
289
+ const outdatedReferences = [];
290
+ let maxConfidence = 0;
291
+ // Check rule age
292
+ const daysSinceModified = (Date.now() - rule.sourceFile.lastModified.getTime()) / (1000 * 60 * 60 * 24);
293
+ if (daysSinceModified > maxAgeDays) {
294
+ outdatedReferences.push({
295
+ reference: `Rule hasn't been updated in ${Math.floor(daysSinceModified)} days`,
296
+ });
297
+ maxConfidence = Math.max(maxConfidence, 0.4);
298
+ }
299
+ // Check for old year references
300
+ const currentYear = new Date().getFullYear();
301
+ const yearPattern = new RegExp(`\\b(20[0-2][0-9])\\b`, 'g');
302
+ let yearMatch;
303
+ while ((yearMatch = yearPattern.exec(rule.content)) !== null) {
304
+ const year = parseInt(yearMatch[1]);
305
+ if (currentYear - year >= oldYearThreshold) {
306
+ outdatedReferences.push({
307
+ reference: `References year ${year} - may need review`,
308
+ });
309
+ maxConfidence = Math.max(maxConfidence, 0.3);
310
+ break; // Only flag once
311
+ }
312
+ }
313
+ // Check user-defined version patterns
314
+ for (const check of versionChecks) {
315
+ try {
316
+ const regex = new RegExp(check.pattern, 'gi');
317
+ const match = regex.exec(rule.content);
318
+ if (match?.[1]) {
319
+ const mentionedVersion = parseFloat(match[1]);
320
+ const currentVersion = parseFloat(check.currentVersion);
321
+ if (mentionedVersion < currentVersion) {
322
+ outdatedReferences.push({
323
+ reference: `${check.name} ${match[1]}`,
324
+ currentVersion: check.currentVersion,
325
+ suggestedUpdate: `${check.name} ${check.currentVersion}`,
326
+ });
327
+ maxConfidence = Math.max(maxConfidence, 0.8);
328
+ }
329
+ }
330
+ }
331
+ catch {
332
+ // Invalid regex pattern, skip
333
+ }
334
+ }
335
+ // Check user-defined deprecation patterns
336
+ for (const pattern of deprecationPatterns) {
337
+ try {
338
+ const regex = new RegExp(pattern.pattern, 'gi');
339
+ if (regex.test(rule.content)) {
340
+ outdatedReferences.push({
341
+ reference: pattern.reason,
342
+ suggestedUpdate: pattern.suggestion,
343
+ });
344
+ maxConfidence = Math.max(maxConfidence, 0.6);
345
+ }
346
+ }
347
+ catch {
348
+ // Invalid regex pattern, skip
349
+ }
350
+ }
351
+ // Flag if rule contains "deprecated" or "legacy" but isn't about migration
352
+ if (/\b(deprecated|legacy|obsolete)\b/i.test(rule.content) &&
353
+ !/\b(migrat|upgrad|replac)\b/i.test(rule.content)) {
354
+ outdatedReferences.push({
355
+ reference: 'Rule discusses deprecated/legacy patterns',
356
+ });
357
+ maxConfidence = Math.max(maxConfidence, 0.5);
358
+ }
359
+ if (outdatedReferences.length > 0) {
360
+ outdated.push({
361
+ rule,
362
+ reason: outdatedReferences.map(r => r.reference).join('; '),
363
+ confidence: maxConfidence,
364
+ action: maxConfidence > 0.7 ? 'update' : 'review',
365
+ outdatedReferences,
366
+ });
367
+ }
368
+ }
369
+ return outdated.sort((a, b) => b.confidence - a.confidence);
370
+ }
371
+ /**
372
+ * Generate merge candidates from clusters and duplicates
373
+ */
374
+ async generateMergeCandidates(clusters, duplicates) {
375
+ const candidates = [];
376
+ // Generate candidates from clusters
377
+ for (const cluster of clusters) {
378
+ if (cluster.rules.length < 2)
379
+ continue;
380
+ if (cluster.confidence < 0.6)
381
+ continue;
382
+ const mergedContent = this.generateSimpleMerge(cluster.rules);
383
+ const tokensBefore = cluster.totalTokensSeparate;
384
+ const tokensAfter = cluster.estimatedTokensMerged;
385
+ candidates.push({
386
+ rules: cluster.rules,
387
+ mergedContent,
388
+ mergedTitle: cluster.suggestedName,
389
+ mergeRationale: `Rules share common topic: ${cluster.topic}`,
390
+ confidence: cluster.confidence,
391
+ tokensBefore,
392
+ tokensAfter,
393
+ preservedFrom: cluster.rules.map(r => ({
394
+ ruleId: r.id,
395
+ preservedConcepts: r.tags,
396
+ })),
397
+ });
398
+ }
399
+ // Generate candidates from high-similarity duplicates
400
+ for (const dup of duplicates) {
401
+ if (dup.recommendation !== 'merge')
402
+ continue;
403
+ if (dup.similarity < 0.8)
404
+ continue;
405
+ const rules = [dup.rule1, dup.rule2];
406
+ const mergedContent = this.generateSimpleMerge(rules);
407
+ const tokensBefore = dup.rule1.tokenCount + dup.rule2.tokenCount;
408
+ const tokensAfter = this.countTokens(mergedContent);
409
+ candidates.push({
410
+ rules,
411
+ mergedContent,
412
+ mergedTitle: dup.rule1.title,
413
+ mergeRationale: `High similarity (${Math.round(dup.similarity * 100)}%) between rules`,
414
+ confidence: dup.similarity,
415
+ tokensBefore,
416
+ tokensAfter,
417
+ preservedFrom: rules.map(r => ({
418
+ ruleId: r.id,
419
+ preservedConcepts: dup.overlappingConcepts,
420
+ })),
421
+ });
422
+ }
423
+ return candidates.sort((a, b) => (b.tokensBefore - b.tokensAfter) - (a.tokensBefore - a.tokensAfter));
424
+ }
425
+ /**
426
+ * Generate simple merged content from rules
427
+ */
428
+ generateSimpleMerge(rules) {
429
+ const sections = rules.map(r => {
430
+ const header = `## ${r.title}`;
431
+ return `${header}\n\n${r.content}`;
432
+ });
433
+ return sections.join('\n\n---\n\n');
434
+ }
435
+ /**
436
+ * Determine match type between rules
437
+ */
438
+ determineMatchType(rule1, rule2, similarity) {
439
+ if (rule1.contentHash === rule2.contentHash)
440
+ return 'exact';
441
+ if (similarity > 0.95)
442
+ return 'near_exact';
443
+ // Check if one is subset of other
444
+ const r1Words = new Set(rule1.content.toLowerCase().split(/\s+/));
445
+ const r2Words = new Set(rule2.content.toLowerCase().split(/\s+/));
446
+ const r1InR2 = [...r1Words].filter(w => r2Words.has(w)).length / r1Words.size;
447
+ const r2InR1 = [...r2Words].filter(w => r1Words.has(w)).length / r2Words.size;
448
+ if (r1InR2 > 0.9 || r2InR1 > 0.9)
449
+ return 'subset';
450
+ return 'semantic';
451
+ }
452
+ /**
453
+ * Find overlapping concepts between rules
454
+ */
455
+ findOverlappingConcepts(rule1, rule2) {
456
+ const concepts = [];
457
+ // Shared tags
458
+ const sharedTags = rule1.tags.filter(t => rule2.tags.includes(t));
459
+ concepts.push(...sharedTags.map(t => `Shared topic: ${t}`));
460
+ // Common code patterns
461
+ const codePatterns = [
462
+ /```(\w+)/g,
463
+ /`([^`]+)`/g,
464
+ ];
465
+ const r1Code = new Set();
466
+ const r2Code = new Set();
467
+ for (const pattern of codePatterns) {
468
+ let match;
469
+ while ((match = pattern.exec(rule1.content)) !== null) {
470
+ if (match[1])
471
+ r1Code.add(match[1]);
472
+ }
473
+ pattern.lastIndex = 0;
474
+ while ((match = pattern.exec(rule2.content)) !== null) {
475
+ if (match[1])
476
+ r2Code.add(match[1]);
477
+ }
478
+ pattern.lastIndex = 0;
479
+ }
480
+ const sharedCode = [...r1Code].filter(c => r2Code.has(c));
481
+ if (sharedCode.length > 0) {
482
+ concepts.push(`Shared code references: ${sharedCode.slice(0, 3).join(', ')}`);
483
+ }
484
+ return concepts.length > 0 ? concepts : ['Similar content'];
485
+ }
486
+ /**
487
+ * Determine recommendation for duplicate
488
+ */
489
+ determineRecommendation(matchType, rule1, rule2) {
490
+ if (matchType === 'exact' || matchType === 'near_exact') {
491
+ // Keep the newer one
492
+ return rule1.sourceFile.lastModified > rule2.sourceFile.lastModified
493
+ ? 'keep_newer'
494
+ : 'keep_newer';
495
+ }
496
+ if (matchType === 'subset') {
497
+ // Keep the more comprehensive one
498
+ return rule1.tokenCount > rule2.tokenCount ? 'keep_specific' : 'keep_specific';
499
+ }
500
+ if (matchType === 'contradicting') {
501
+ return 'resolve_conflict';
502
+ }
503
+ return 'merge';
504
+ }
505
+ /**
506
+ * Detect conflict between two rules
507
+ */
508
+ detectConflict(rule1, rule2, patterns) {
509
+ const conflictingStatements = [];
510
+ let conflictType = null;
511
+ // Extract directive statements
512
+ const r1Directives = this.extractDirectives(rule1.content);
513
+ const r2Directives = this.extractDirectives(rule2.content);
514
+ // Check for contradicting directives
515
+ for (const d1 of r1Directives) {
516
+ for (const d2 of r2Directives) {
517
+ if (this.directivesContradict(d1, d2)) {
518
+ conflictingStatements.push({
519
+ from: rule1.title,
520
+ statement1: d1,
521
+ statement2: d2,
522
+ });
523
+ conflictType = 'direct_contradiction';
524
+ }
525
+ }
526
+ }
527
+ // Check for preference conflicts
528
+ const r1Preferences = this.extractPreferences(rule1.content);
529
+ const r2Preferences = this.extractPreferences(rule2.content);
530
+ for (const [topic, pref1] of r1Preferences) {
531
+ const pref2 = r2Preferences.get(topic);
532
+ if (pref2 && pref1 !== pref2) {
533
+ conflictingStatements.push({
534
+ from: rule1.title,
535
+ statement1: `Prefer ${pref1} for ${topic}`,
536
+ statement2: `Prefer ${pref2} for ${topic}`,
537
+ });
538
+ conflictType = conflictType ?? 'preference_conflict';
539
+ }
540
+ }
541
+ if (conflictingStatements.length === 0)
542
+ return null;
543
+ return {
544
+ rule1,
545
+ rule2,
546
+ conflictType: conflictType,
547
+ description: `${conflictingStatements.length} conflicting statement(s) found`,
548
+ conflictingStatements,
549
+ resolution: {
550
+ preferredRuleId: rule1.sourceFile.lastModified > rule2.sourceFile.lastModified
551
+ ? rule1.id
552
+ : rule2.id,
553
+ reasoning: 'Prefer the more recently modified rule',
554
+ },
555
+ };
556
+ }
557
+ /**
558
+ * Extract directive statements from content
559
+ */
560
+ extractDirectives(content) {
561
+ const directives = [];
562
+ const patterns = [
563
+ /(?:always|never|must|must not|do not|don't)\s+[^.]+\./gi,
564
+ /(?:use|prefer|avoid)\s+[^.]+\./gi,
565
+ ];
566
+ for (const pattern of patterns) {
567
+ let match;
568
+ while ((match = pattern.exec(content)) !== null) {
569
+ directives.push(match[0].trim());
570
+ }
571
+ }
572
+ return directives;
573
+ }
574
+ /**
575
+ * Extract preference statements
576
+ */
577
+ extractPreferences(content) {
578
+ const preferences = new Map();
579
+ const pattern = /prefer\s+(\w+)\s+(?:over|instead of|rather than)\s+(\w+)/gi;
580
+ let match;
581
+ while ((match = pattern.exec(content)) !== null) {
582
+ if (match[1] && match[2]) {
583
+ preferences.set(match[2].toLowerCase(), match[1].toLowerCase());
584
+ }
585
+ }
586
+ return preferences;
587
+ }
588
+ /**
589
+ * Check if two directives contradict
590
+ */
591
+ directivesContradict(d1, d2) {
592
+ const d1Lower = d1.toLowerCase();
593
+ const d2Lower = d2.toLowerCase();
594
+ // Check for opposite modifiers
595
+ const alwaysNever = ((d1Lower.includes('always') && d2Lower.includes('never')) ||
596
+ (d1Lower.includes('never') && d2Lower.includes('always')));
597
+ const mustMustNot = ((d1Lower.includes('must ') && d2Lower.includes('must not')) ||
598
+ (d1Lower.includes('must not') && d2Lower.includes('must ')));
599
+ // Check if they're about the same topic
600
+ const d1Words = new Set(d1Lower.split(/\W+/).filter(w => w.length > 3));
601
+ const d2Words = new Set(d2Lower.split(/\W+/).filter(w => w.length > 3));
602
+ const overlap = [...d1Words].filter(w => d2Words.has(w));
603
+ return (alwaysNever || mustMustNot) && overlap.length > 2;
604
+ }
605
+ /**
606
+ * Calculate average similarity within a cluster
607
+ */
608
+ calculateAverageClusterSimilarity(rules) {
609
+ if (rules.length < 2)
610
+ return 0;
611
+ let totalSim = 0;
612
+ let count = 0;
613
+ for (let i = 0; i < rules.length; i++) {
614
+ for (let j = i + 1; j < rules.length; j++) {
615
+ const emb1 = this.ruleEmbeddings.get(rules[i].id);
616
+ const emb2 = this.ruleEmbeddings.get(rules[j].id);
617
+ if (emb1 && emb2) {
618
+ totalSim += cosineSimilarity(emb1, emb2);
619
+ count++;
620
+ }
621
+ }
622
+ }
623
+ return count > 0 ? totalSim / count : 0;
624
+ }
625
+ /**
626
+ * Estimate token savings from optimizations
627
+ */
628
+ estimateSavings(merges, duplicates) {
629
+ let savings = 0;
630
+ // Savings from merges
631
+ for (const merge of merges) {
632
+ savings += merge.tokensBefore - merge.tokensAfter;
633
+ }
634
+ // Savings from duplicate removal (keep one)
635
+ for (const dup of duplicates) {
636
+ if (dup.recommendation === 'keep_newer' || dup.recommendation === 'keep_specific') {
637
+ savings += Math.min(dup.rule1.tokenCount, dup.rule2.tokenCount);
638
+ }
639
+ }
640
+ return Math.max(0, savings);
641
+ }
642
+ /**
643
+ * Build optimization plan
644
+ */
645
+ buildOptimizationPlan(duplicates, merges, conflicts, outdated) {
646
+ const actions = [];
647
+ let priority = 1;
648
+ // Add actions for exact duplicates (safe to auto-apply)
649
+ for (const dup of duplicates.filter(d => d.matchType === 'exact')) {
650
+ const ruleToRemove = dup.rule1.sourceFile.lastModified < dup.rule2.sourceFile.lastModified
651
+ ? dup.rule1
652
+ : dup.rule2;
653
+ actions.push({
654
+ type: 'delete',
655
+ description: `Remove exact duplicate: ${ruleToRemove.title}`,
656
+ affectedRules: [ruleToRemove.id],
657
+ affectedFiles: [ruleToRemove.sourceFile.path],
658
+ tokenImpact: -ruleToRemove.tokenCount,
659
+ priority: priority++,
660
+ autoApplyable: true,
661
+ });
662
+ }
663
+ // Add actions for high-confidence merges
664
+ for (const merge of merges.filter(m => m.confidence > 0.8)) {
665
+ actions.push({
666
+ type: 'merge',
667
+ description: `Merge ${merge.rules.length} rules: ${merge.mergedTitle}`,
668
+ affectedRules: merge.rules.map(r => r.id),
669
+ affectedFiles: [...new Set(merge.rules.map(r => r.sourceFile.path))],
670
+ tokenImpact: -(merge.tokensBefore - merge.tokensAfter),
671
+ priority: priority++,
672
+ autoApplyable: merge.confidence > 0.9,
673
+ });
674
+ }
675
+ // Add actions for conflicts (require review)
676
+ for (const conflict of conflicts) {
677
+ actions.push({
678
+ type: 'review',
679
+ description: `Resolve conflict: ${conflict.description}`,
680
+ affectedRules: [conflict.rule1.id, conflict.rule2.id],
681
+ affectedFiles: [conflict.rule1.sourceFile.path, conflict.rule2.sourceFile.path],
682
+ tokenImpact: 0,
683
+ priority: priority++,
684
+ autoApplyable: false,
685
+ });
686
+ }
687
+ // Add actions for outdated rules
688
+ for (const out of outdated) {
689
+ actions.push({
690
+ type: out.action === 'remove' ? 'delete' : 'update',
691
+ description: `${out.action === 'remove' ? 'Remove' : 'Update'} outdated: ${out.rule.title}`,
692
+ affectedRules: [out.rule.id],
693
+ affectedFiles: [out.rule.sourceFile.path],
694
+ tokenImpact: out.action === 'remove' ? -out.rule.tokenCount : 0,
695
+ priority: priority++,
696
+ autoApplyable: false,
697
+ });
698
+ }
699
+ const hasManualReview = actions.some(a => !a.autoApplyable);
700
+ const riskLevel = conflicts.length > 0 ? 'high' : (duplicates.length > 5 ? 'medium' : 'low');
701
+ return {
702
+ actions,
703
+ estimatedDuration: `${Math.ceil(actions.length * 0.5)} minutes`,
704
+ riskLevel,
705
+ requiresManualReview: hasManualReview,
706
+ };
707
+ }
708
+ /**
709
+ * Build file changes from plan
710
+ */
711
+ buildFileChanges(plan, rules) {
712
+ const changes = [];
713
+ const ruleMap = new Map(rules.map(r => [r.id, r]));
714
+ for (const action of plan.actions) {
715
+ if (action.type === 'delete') {
716
+ for (const ruleId of action.affectedRules) {
717
+ const rule = ruleMap.get(ruleId);
718
+ if (rule) {
719
+ changes.push({
720
+ path: rule.sourceFile.path,
721
+ changeType: 'delete',
722
+ originalContent: rule.sourceFile.content,
723
+ });
724
+ }
725
+ }
726
+ }
727
+ else if (action.type === 'merge') {
728
+ // First file gets merged content
729
+ const firstFile = action.affectedFiles[0];
730
+ if (firstFile) {
731
+ changes.push({
732
+ path: firstFile,
733
+ changeType: 'modify',
734
+ newContent: '/* Merged content - see optimization report */',
735
+ });
736
+ }
737
+ // Other files deleted
738
+ for (const file of action.affectedFiles.slice(1)) {
739
+ changes.push({
740
+ path: file,
741
+ changeType: 'delete',
742
+ });
743
+ }
744
+ }
745
+ }
746
+ return changes;
747
+ }
748
+ /**
749
+ * Simple token counting
750
+ */
751
+ countTokens(content) {
752
+ // Rough estimate: 4 chars per token
753
+ return Math.ceil(content.length / 4);
754
+ }
755
+ }
756
+ /**
757
+ * Singleton instance
758
+ */
759
+ let analyzerInstance = null;
760
+ export function getRulesAnalyzer(config, options, rulesConfig) {
761
+ if (!analyzerInstance || options || rulesConfig) {
762
+ analyzerInstance = new RulesAnalyzer(config, options, rulesConfig);
763
+ }
764
+ return analyzerInstance;
765
+ }
766
+ // Re-export config types and functions
767
+ export { loadRulesConfig, saveRulesConfig, validatePattern, testPattern, EXAMPLE_PATTERNS, } from '../config/rulesConfig.js';
768
+ //# sourceMappingURL=rulesAnalyzer.js.map