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.
- package/README.md +179 -203
- package/dist/adapters/llm/anthropic.d.ts +27 -0
- package/dist/adapters/llm/anthropic.d.ts.map +1 -0
- package/dist/adapters/llm/anthropic.js +287 -0
- package/dist/adapters/llm/anthropic.js.map +1 -0
- package/dist/adapters/llm/base.d.ts +62 -0
- package/dist/adapters/llm/base.d.ts.map +1 -0
- package/dist/adapters/llm/base.js +140 -0
- package/dist/adapters/llm/base.js.map +1 -0
- package/dist/adapters/llm/deepseek.d.ts +24 -0
- package/dist/adapters/llm/deepseek.d.ts.map +1 -0
- package/dist/adapters/llm/deepseek.js +228 -0
- package/dist/adapters/llm/deepseek.js.map +1 -0
- package/dist/adapters/llm/groq.d.ts +25 -0
- package/dist/adapters/llm/groq.d.ts.map +1 -0
- package/dist/adapters/llm/groq.js +265 -0
- package/dist/adapters/llm/groq.js.map +1 -0
- package/dist/adapters/llm/index.d.ts +62 -0
- package/dist/adapters/llm/index.d.ts.map +1 -0
- package/dist/adapters/llm/index.js +380 -0
- package/dist/adapters/llm/index.js.map +1 -0
- package/dist/adapters/llm/ollama.d.ts +23 -0
- package/dist/adapters/llm/ollama.d.ts.map +1 -0
- package/dist/adapters/llm/ollama.js +261 -0
- package/dist/adapters/llm/ollama.js.map +1 -0
- package/dist/adapters/llm/openai.d.ts +22 -0
- package/dist/adapters/llm/openai.d.ts.map +1 -0
- package/dist/adapters/llm/openai.js +232 -0
- package/dist/adapters/llm/openai.js.map +1 -0
- package/dist/adapters/llm/openrouter.d.ts +27 -0
- package/dist/adapters/llm/openrouter.d.ts.map +1 -0
- package/dist/adapters/llm/openrouter.js +305 -0
- package/dist/adapters/llm/openrouter.js.map +1 -0
- package/dist/adapters/vector/index.d.ts.map +1 -1
- package/dist/adapters/vector/index.js +8 -0
- package/dist/adapters/vector/index.js.map +1 -1
- package/dist/adapters/vector/redis-native.d.ts +35 -0
- package/dist/adapters/vector/redis-native.d.ts.map +1 -0
- package/dist/adapters/vector/redis-native.js +170 -0
- package/dist/adapters/vector/redis-native.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +374 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/maintenance.d.ts +4 -0
- package/dist/cli/commands/maintenance.d.ts.map +1 -0
- package/dist/cli/commands/maintenance.js +237 -0
- package/dist/cli/commands/maintenance.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +9 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +639 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/setup.js +5 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/memoryConfig.d.ts +427 -0
- package/dist/config/memoryConfig.d.ts.map +1 -0
- package/dist/config/memoryConfig.js +258 -0
- package/dist/config/memoryConfig.js.map +1 -0
- package/dist/config/rulesConfig.d.ts +486 -0
- package/dist/config/rulesConfig.d.ts.map +1 -0
- package/dist/config/rulesConfig.js +345 -0
- package/dist/config/rulesConfig.js.map +1 -0
- package/dist/dashboard/coreTools.d.ts +14 -0
- package/dist/dashboard/coreTools.d.ts.map +1 -0
- package/dist/dashboard/coreTools.js +413 -0
- package/dist/dashboard/coreTools.js.map +1 -0
- package/dist/dashboard/public/index.html +1982 -13
- package/dist/dashboard/server.d.ts +1 -8
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +846 -13
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/toolRegistry.d.ts +192 -0
- package/dist/dashboard/toolRegistry.d.ts.map +1 -0
- package/dist/dashboard/toolRegistry.js +322 -0
- package/dist/dashboard/toolRegistry.js.map +1 -0
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +9 -6
- package/dist/proxy/index.js.map +1 -1
- package/dist/server/index.js +21 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/tools/crawl.d.ts.map +1 -1
- package/dist/server/tools/crawl.js +8 -0
- package/dist/server/tools/crawl.js.map +1 -1
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +19 -1
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/ingest.d.ts.map +1 -1
- package/dist/server/tools/ingest.js +5 -0
- package/dist/server/tools/ingest.js.map +1 -1
- package/dist/server/tools/memory.d.ts +250 -0
- package/dist/server/tools/memory.d.ts.map +1 -0
- package/dist/server/tools/memory.js +472 -0
- package/dist/server/tools/memory.js.map +1 -0
- package/dist/server/tools/recursive-query.d.ts.map +1 -1
- package/dist/server/tools/recursive-query.js +6 -0
- package/dist/server/tools/recursive-query.js.map +1 -1
- package/dist/server/tools/search.d.ts.map +1 -1
- package/dist/server/tools/search.js +6 -0
- package/dist/server/tools/search.js.map +1 -1
- package/dist/services/activity-log.d.ts +10 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +53 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/categoryManager.d.ts +110 -0
- package/dist/services/categoryManager.d.ts.map +1 -0
- package/dist/services/categoryManager.js +549 -0
- package/dist/services/categoryManager.js.map +1 -0
- package/dist/services/contextEnvironment.d.ts +206 -0
- package/dist/services/contextEnvironment.d.ts.map +1 -0
- package/dist/services/contextEnvironment.js +481 -0
- package/dist/services/contextEnvironment.js.map +1 -0
- package/dist/services/conversationProcessor.d.ts +99 -0
- package/dist/services/conversationProcessor.d.ts.map +1 -0
- package/dist/services/conversationProcessor.js +311 -0
- package/dist/services/conversationProcessor.js.map +1 -0
- package/dist/services/cursorChatReader.d.ts +129 -0
- package/dist/services/cursorChatReader.d.ts.map +1 -0
- package/dist/services/cursorChatReader.js +419 -0
- package/dist/services/cursorChatReader.js.map +1 -0
- package/dist/services/decayCalculator.d.ts +85 -0
- package/dist/services/decayCalculator.d.ts.map +1 -0
- package/dist/services/decayCalculator.js +182 -0
- package/dist/services/decayCalculator.js.map +1 -0
- package/dist/services/enhancedVectorStore.d.ts +102 -0
- package/dist/services/enhancedVectorStore.d.ts.map +1 -0
- package/dist/services/enhancedVectorStore.js +245 -0
- package/dist/services/enhancedVectorStore.js.map +1 -0
- package/dist/services/hybridScorer.d.ts +120 -0
- package/dist/services/hybridScorer.d.ts.map +1 -0
- package/dist/services/hybridScorer.js +334 -0
- package/dist/services/hybridScorer.js.map +1 -0
- package/dist/services/knowledgeExtractor.d.ts +45 -0
- package/dist/services/knowledgeExtractor.d.ts.map +1 -0
- package/dist/services/knowledgeExtractor.js +436 -0
- package/dist/services/knowledgeExtractor.js.map +1 -0
- package/dist/services/knowledgeStorage.d.ts +102 -0
- package/dist/services/knowledgeStorage.d.ts.map +1 -0
- package/dist/services/knowledgeStorage.js +383 -0
- package/dist/services/knowledgeStorage.js.map +1 -0
- package/dist/services/maintenanceScheduler.d.ts +89 -0
- package/dist/services/maintenanceScheduler.d.ts.map +1 -0
- package/dist/services/maintenanceScheduler.js +479 -0
- package/dist/services/maintenanceScheduler.js.map +1 -0
- package/dist/services/memoryMetadataStore.d.ts +62 -0
- package/dist/services/memoryMetadataStore.d.ts.map +1 -0
- package/dist/services/memoryMetadataStore.js +570 -0
- package/dist/services/memoryMetadataStore.js.map +1 -0
- package/dist/services/recursiveRetrieval.d.ts +122 -0
- package/dist/services/recursiveRetrieval.d.ts.map +1 -0
- package/dist/services/recursiveRetrieval.js +443 -0
- package/dist/services/recursiveRetrieval.js.map +1 -0
- package/dist/services/relationshipGraph.d.ts +77 -0
- package/dist/services/relationshipGraph.d.ts.map +1 -0
- package/dist/services/relationshipGraph.js +411 -0
- package/dist/services/relationshipGraph.js.map +1 -0
- package/dist/services/rlmSafeguards.d.ts +273 -0
- package/dist/services/rlmSafeguards.d.ts.map +1 -0
- package/dist/services/rlmSafeguards.js +705 -0
- package/dist/services/rlmSafeguards.js.map +1 -0
- package/dist/services/rulesAnalyzer.d.ts +119 -0
- package/dist/services/rulesAnalyzer.d.ts.map +1 -0
- package/dist/services/rulesAnalyzer.js +768 -0
- package/dist/services/rulesAnalyzer.js.map +1 -0
- package/dist/services/rulesMerger.d.ts +75 -0
- package/dist/services/rulesMerger.d.ts.map +1 -0
- package/dist/services/rulesMerger.js +404 -0
- package/dist/services/rulesMerger.js.map +1 -0
- package/dist/services/rulesParser.d.ts +127 -0
- package/dist/services/rulesParser.d.ts.map +1 -0
- package/dist/services/rulesParser.js +594 -0
- package/dist/services/rulesParser.js.map +1 -0
- package/dist/services/smartChunker.d.ts +110 -0
- package/dist/services/smartChunker.d.ts.map +1 -0
- package/dist/services/smartChunker.js +520 -0
- package/dist/services/smartChunker.js.map +1 -0
- package/dist/types/categories.d.ts +105 -0
- package/dist/types/categories.d.ts.map +1 -0
- package/dist/types/categories.js +108 -0
- package/dist/types/categories.js.map +1 -0
- package/dist/types/extractedKnowledge.d.ts +233 -0
- package/dist/types/extractedKnowledge.d.ts.map +1 -0
- package/dist/types/extractedKnowledge.js +56 -0
- package/dist/types/extractedKnowledge.js.map +1 -0
- package/dist/types/index.d.ts +9 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +12 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/llmProvider.d.ts +282 -0
- package/dist/types/llmProvider.d.ts.map +1 -0
- package/dist/types/llmProvider.js +48 -0
- package/dist/types/llmProvider.js.map +1 -0
- package/dist/types/memory.d.ts +227 -0
- package/dist/types/memory.d.ts.map +1 -0
- package/dist/types/memory.js +76 -0
- package/dist/types/memory.js.map +1 -0
- package/dist/types/relationships.d.ts +167 -0
- package/dist/types/relationships.d.ts.map +1 -0
- package/dist/types/relationships.js +106 -0
- package/dist/types/relationships.js.map +1 -0
- package/dist/types/rulesOptimizer.d.ts +345 -0
- package/dist/types/rulesOptimizer.d.ts.map +1 -0
- package/dist/types/rulesOptimizer.js +22 -0
- package/dist/types/rulesOptimizer.js.map +1 -0
- package/docs/cursor-recursive-rag-memory-spec.md +4569 -0
- package/docs/cursor-recursive-rag-tasks.md +1355 -0
- package/package.json +6 -3
- 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
|