@iservu-inc/adf-cli 0.3.0 → 0.4.12

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 (36) hide show
  1. package/.project/chats/{current → complete}/2025-10-03_AGENTS-MD-AND-TOOL-GENERATORS.md +82 -17
  2. package/.project/chats/complete/2025-10-03_AI-PROVIDER-INTEGRATION.md +568 -0
  3. package/.project/chats/complete/2025-10-03_FRAMEWORK-UPDATE-SYSTEM.md +497 -0
  4. package/.project/chats/complete/2025-10-04_CONFIG-COMMAND.md +503 -0
  5. package/.project/chats/current/2025-10-04_PHASE-4-1-SMART-FILTERING.md +381 -0
  6. package/.project/chats/current/SESSION-STATUS.md +168 -0
  7. package/.project/docs/AI-PROVIDER-INTEGRATION.md +600 -0
  8. package/.project/docs/FRAMEWORK-UPDATE-INTEGRATION.md +421 -0
  9. package/.project/docs/FRAMEWORK-UPDATE-SYSTEM.md +832 -0
  10. package/.project/docs/PHASE-4-2-LEARNING-SYSTEM.md +881 -0
  11. package/.project/docs/PROJECT-STRUCTURE-EXPLANATION.md +500 -0
  12. package/.project/docs/SMART-FILTERING-SYSTEM.md +385 -0
  13. package/.project/docs/architecture/SYSTEM-DESIGN.md +122 -1
  14. package/.project/docs/goals/PROJECT-VISION.md +61 -34
  15. package/CHANGELOG.md +257 -1
  16. package/README.md +476 -292
  17. package/bin/adf.js +7 -0
  18. package/lib/ai/ai-client.js +328 -0
  19. package/lib/ai/ai-config.js +398 -0
  20. package/lib/analyzers/project-analyzer.js +380 -0
  21. package/lib/commands/config.js +221 -0
  22. package/lib/commands/init.js +56 -10
  23. package/lib/filters/question-filter.js +480 -0
  24. package/lib/frameworks/interviewer.js +271 -12
  25. package/lib/frameworks/progress-tracker.js +8 -1
  26. package/lib/learning/learning-manager.js +447 -0
  27. package/lib/learning/pattern-detector.js +376 -0
  28. package/lib/learning/rule-generator.js +304 -0
  29. package/lib/learning/skip-tracker.js +260 -0
  30. package/lib/learning/storage.js +296 -0
  31. package/package.json +70 -57
  32. package/tests/learning-storage.test.js +184 -0
  33. package/tests/pattern-detector.test.js +297 -0
  34. package/tests/project-analyzer.test.js +221 -0
  35. package/tests/question-filter.test.js +297 -0
  36. package/tests/skip-tracker.test.js +198 -0
@@ -0,0 +1,376 @@
1
+ const storage = require('./storage');
2
+ const { v4: uuidv4 } = require('uuid');
3
+
4
+ /**
5
+ * Pattern Detector - Analyzes skip/answer history to detect patterns
6
+ *
7
+ * Detects:
8
+ * - Consistent skip patterns (same question skipped repeatedly)
9
+ * - Category skip patterns (user skips most questions in a category)
10
+ * - Framework-specific patterns (framework + question combinations)
11
+ * - User preferences (answer style, detail level)
12
+ */
13
+
14
+ class PatternDetector {
15
+ constructor(skipHistory, answerHistory, config = {}) {
16
+ this.skipHistory = skipHistory;
17
+ this.answerHistory = answerHistory;
18
+ this.config = {
19
+ minSessionsForPattern: config.minSessionsForPattern || 3,
20
+ minConfidenceForAutoFilter: config.minConfidenceForAutoFilter || 75,
21
+ ...config
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Detect all patterns
27
+ * @returns {Object} All detected patterns
28
+ */
29
+ detectAllPatterns() {
30
+ return {
31
+ consistentSkips: this.detectConsistentSkips(),
32
+ categoryPatterns: this.detectCategoryPatterns(),
33
+ frameworkPatterns: this.detectFrameworkPatterns(),
34
+ userPreferences: this.detectUserPreferences()
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Detect consistent skip patterns
40
+ * @returns {Array} Consistent skip patterns
41
+ */
42
+ detectConsistentSkips() {
43
+ const patterns = [];
44
+ const questionSkipData = this.aggregateQuestionSkips();
45
+
46
+ for (const [questionId, data] of Object.entries(questionSkipData)) {
47
+ const totalSessions = this.skipHistory.sessions.length;
48
+ const skipCount = data.skipCount;
49
+ const confidence = Math.round((skipCount / totalSessions) * 100);
50
+
51
+ // Pattern threshold: skipped in 75%+ of sessions with minimum sessions
52
+ if (skipCount >= this.config.minSessionsForPattern && confidence >= 75) {
53
+ patterns.push({
54
+ id: `pattern_skip_${questionId}`,
55
+ type: 'consistent_skip',
56
+ questionId,
57
+ questionText: data.text,
58
+ category: data.category,
59
+ confidence,
60
+ sessionsAnalyzed: totalSessions,
61
+ skipCount,
62
+ recommendation: confidence >= this.config.minConfidenceForAutoFilter
63
+ ? 'Auto-filter this question'
64
+ : 'Monitor for more sessions',
65
+ status: 'detected',
66
+ userApproved: false
67
+ });
68
+ }
69
+ }
70
+
71
+ return patterns.sort((a, b) => b.confidence - a.confidence);
72
+ }
73
+
74
+ /**
75
+ * Detect category skip patterns
76
+ * @returns {Array} Category skip patterns
77
+ */
78
+ detectCategoryPatterns() {
79
+ const patterns = [];
80
+ const categoryData = this.aggregateCategoryData();
81
+
82
+ for (const [category, data] of Object.entries(categoryData)) {
83
+ if (data.totalQuestions === 0) continue;
84
+
85
+ const skipRate = Math.round((data.skipCount / data.totalQuestions) * 100);
86
+
87
+ // Pattern threshold: 70%+ skip rate in category with minimum appearances
88
+ if (data.totalQuestions >= 5 && skipRate >= 70) {
89
+ patterns.push({
90
+ id: `pattern_category_${category}`,
91
+ type: 'category_skip',
92
+ category,
93
+ confidence: skipRate,
94
+ totalQuestions: data.totalQuestions,
95
+ skipCount: data.skipCount,
96
+ recommendation: skipRate >= this.config.minConfidenceForAutoFilter
97
+ ? `Reduce relevance for ${category} questions`
98
+ : 'Monitor category skip behavior',
99
+ status: 'detected',
100
+ userApproved: false
101
+ });
102
+ }
103
+ }
104
+
105
+ return patterns.sort((a, b) => b.confidence - a.confidence);
106
+ }
107
+
108
+ /**
109
+ * Detect framework-specific skip patterns
110
+ * @returns {Array} Framework skip patterns
111
+ */
112
+ detectFrameworkPatterns() {
113
+ const patterns = [];
114
+ const frameworkSkipData = this.aggregateFrameworkSkips();
115
+
116
+ for (const [framework, questions] of Object.entries(frameworkSkipData)) {
117
+ for (const [questionId, data] of Object.entries(questions)) {
118
+ const frameworkSessions = this.skipHistory.sessions.filter(
119
+ s => s.frameworks && s.frameworks.includes(framework)
120
+ ).length;
121
+
122
+ if (frameworkSessions < this.config.minSessionsForPattern) continue;
123
+
124
+ const confidence = Math.round((data.skipCount / frameworkSessions) * 100);
125
+
126
+ // Pattern threshold: 75%+ skip rate for framework + question combo
127
+ if (confidence >= 75) {
128
+ patterns.push({
129
+ id: `pattern_framework_${framework}_${questionId}`,
130
+ type: 'framework_skip',
131
+ framework,
132
+ questionId,
133
+ questionText: data.text,
134
+ category: data.category,
135
+ confidence,
136
+ sessionsAnalyzed: frameworkSessions,
137
+ skipCount: data.skipCount,
138
+ recommendation: `Skip "${data.text}" for ${framework} projects`,
139
+ status: 'detected',
140
+ userApproved: false
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ return patterns.sort((a, b) => b.confidence - a.confidence);
147
+ }
148
+
149
+ /**
150
+ * Detect user preference patterns
151
+ * @returns {Array} User preference patterns
152
+ */
153
+ detectUserPreferences() {
154
+ const patterns = [];
155
+
156
+ if (!this.answerHistory || !this.answerHistory.sessions) {
157
+ return patterns;
158
+ }
159
+
160
+ // Analyze answer length by category
161
+ const categoryAnswerData = {};
162
+
163
+ for (const session of this.answerHistory.sessions) {
164
+ for (const answer of session.answers || []) {
165
+ if (!answer.category) continue;
166
+
167
+ if (!categoryAnswerData[answer.category]) {
168
+ categoryAnswerData[answer.category] = {
169
+ totalAnswers: 0,
170
+ totalWords: 0,
171
+ avgWords: 0
172
+ };
173
+ }
174
+
175
+ categoryAnswerData[answer.category].totalAnswers++;
176
+ categoryAnswerData[answer.category].totalWords += answer.wordCount || 0;
177
+ }
178
+ }
179
+
180
+ // Detect brief answer preferences
181
+ for (const [category, data] of Object.entries(categoryAnswerData)) {
182
+ if (data.totalAnswers < 3) continue;
183
+
184
+ data.avgWords = Math.round(data.totalWords / data.totalAnswers);
185
+
186
+ // Brief answers: <30 words on average
187
+ if (data.avgWords < 30) {
188
+ patterns.push({
189
+ id: `pattern_pref_brief_${category}`,
190
+ type: 'user_preference',
191
+ category,
192
+ preference: 'brief_answers',
193
+ avgWordCount: data.avgWords,
194
+ confidence: 70,
195
+ recommendation: `User prefers brief answers for ${category} questions`,
196
+ status: 'detected',
197
+ userApproved: false
198
+ });
199
+ }
200
+ // Detailed answers: >100 words on average
201
+ else if (data.avgWords > 100) {
202
+ patterns.push({
203
+ id: `pattern_pref_detailed_${category}`,
204
+ type: 'user_preference',
205
+ category,
206
+ preference: 'detailed_answers',
207
+ avgWordCount: data.avgWords,
208
+ confidence: 70,
209
+ recommendation: `User prefers detailed answers for ${category} questions`,
210
+ status: 'detected',
211
+ userApproved: false
212
+ });
213
+ }
214
+ }
215
+
216
+ return patterns;
217
+ }
218
+
219
+ /**
220
+ * Aggregate question skip data
221
+ * @returns {Object} Question skip data
222
+ */
223
+ aggregateQuestionSkips() {
224
+ const questionData = {};
225
+
226
+ for (const session of this.skipHistory.sessions) {
227
+ const sessionQuestions = new Set(); // Track unique questions per session
228
+
229
+ for (const skip of session.skips || []) {
230
+ // Only count manual skips for pattern detection
231
+ if (skip.reason !== 'manual') continue;
232
+
233
+ if (!questionData[skip.questionId]) {
234
+ questionData[skip.questionId] = {
235
+ text: skip.text,
236
+ category: skip.category,
237
+ skipCount: 0,
238
+ sessions: []
239
+ };
240
+ }
241
+
242
+ // Only count once per session
243
+ if (!sessionQuestions.has(skip.questionId)) {
244
+ questionData[skip.questionId].skipCount++;
245
+ questionData[skip.questionId].sessions.push(session.sessionId);
246
+ sessionQuestions.add(skip.questionId);
247
+ }
248
+ }
249
+ }
250
+
251
+ return questionData;
252
+ }
253
+
254
+ /**
255
+ * Aggregate category data (total questions vs skips)
256
+ * @returns {Object} Category data
257
+ */
258
+ aggregateCategoryData() {
259
+ const categoryData = {};
260
+
261
+ for (const session of this.skipHistory.sessions) {
262
+ // Count all questions by category (skipped + answered)
263
+ const sessionCategories = {};
264
+
265
+ for (const skip of session.skips || []) {
266
+ if (!skip.category) continue;
267
+
268
+ if (!sessionCategories[skip.category]) {
269
+ sessionCategories[skip.category] = { total: 0, skipped: 0 };
270
+ }
271
+
272
+ sessionCategories[skip.category].total++;
273
+ if (skip.reason === 'manual') {
274
+ sessionCategories[skip.category].skipped++;
275
+ }
276
+ }
277
+
278
+ // Aggregate across sessions
279
+ for (const [category, counts] of Object.entries(sessionCategories)) {
280
+ if (!categoryData[category]) {
281
+ categoryData[category] = { totalQuestions: 0, skipCount: 0 };
282
+ }
283
+
284
+ categoryData[category].totalQuestions += counts.total;
285
+ categoryData[category].skipCount += counts.skipped;
286
+ }
287
+ }
288
+
289
+ return categoryData;
290
+ }
291
+
292
+ /**
293
+ * Aggregate framework-specific skip data
294
+ * @returns {Object} Framework skip data
295
+ */
296
+ aggregateFrameworkSkips() {
297
+ const frameworkData = {};
298
+
299
+ for (const session of this.skipHistory.sessions) {
300
+ const frameworks = session.frameworks || [];
301
+
302
+ for (const framework of frameworks) {
303
+ if (!frameworkData[framework]) {
304
+ frameworkData[framework] = {};
305
+ }
306
+
307
+ for (const skip of session.skips || []) {
308
+ if (skip.reason !== 'manual') continue;
309
+
310
+ if (!frameworkData[framework][skip.questionId]) {
311
+ frameworkData[framework][skip.questionId] = {
312
+ text: skip.text,
313
+ category: skip.category,
314
+ skipCount: 0
315
+ };
316
+ }
317
+
318
+ frameworkData[framework][skip.questionId].skipCount++;
319
+ }
320
+ }
321
+ }
322
+
323
+ return frameworkData;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Detect patterns from project history
329
+ * @param {string} projectPath - Project root path
330
+ * @returns {Promise<Object>} Detected patterns
331
+ */
332
+ async function detectPatterns(projectPath) {
333
+ const skipHistory = await storage.getSkipHistory(projectPath);
334
+ const answerHistory = await storage.getAnswerHistory(projectPath);
335
+ const config = await storage.getLearningConfig(projectPath);
336
+
337
+ const detector = new PatternDetector(skipHistory, answerHistory, config);
338
+ return detector.detectAllPatterns();
339
+ }
340
+
341
+ /**
342
+ * Get pattern summary statistics
343
+ * @param {Object} patterns - Detected patterns
344
+ * @returns {Object} Summary statistics
345
+ */
346
+ function getPatternSummary(patterns) {
347
+ const allPatterns = [
348
+ ...(patterns.consistentSkips || []),
349
+ ...(patterns.categoryPatterns || []),
350
+ ...(patterns.frameworkPatterns || []),
351
+ ...(patterns.userPreferences || [])
352
+ ];
353
+
354
+ const highConfidence = allPatterns.filter(p => p.confidence >= 80).length;
355
+ const mediumConfidence = allPatterns.filter(p => p.confidence >= 60 && p.confidence < 80).length;
356
+ const lowConfidence = allPatterns.filter(p => p.confidence < 60).length;
357
+
358
+ return {
359
+ total: allPatterns.length,
360
+ highConfidence,
361
+ mediumConfidence,
362
+ lowConfidence,
363
+ byType: {
364
+ consistentSkips: patterns.consistentSkips?.length || 0,
365
+ categoryPatterns: patterns.categoryPatterns?.length || 0,
366
+ frameworkPatterns: patterns.frameworkPatterns?.length || 0,
367
+ userPreferences: patterns.userPreferences?.length || 0
368
+ }
369
+ };
370
+ }
371
+
372
+ module.exports = {
373
+ PatternDetector,
374
+ detectPatterns,
375
+ getPatternSummary
376
+ };
@@ -0,0 +1,304 @@
1
+ const storage = require('./storage');
2
+
3
+ /**
4
+ * Rule Generator - Converts detected patterns into filtering rules
5
+ *
6
+ * Generates rules like:
7
+ * - Skip specific questions
8
+ * - Reduce relevance for categories
9
+ * - Framework-specific adjustments
10
+ * - User preference adjustments
11
+ */
12
+
13
+ /**
14
+ * Generate filtering rules from patterns
15
+ * @param {Object} patterns - Detected patterns
16
+ * @param {Object} config - Learning configuration
17
+ * @returns {Array} Generated rules
18
+ */
19
+ function generateRulesFromPatterns(patterns, config = {}) {
20
+ const rules = [];
21
+ const minConfidence = config.minConfidenceForAutoFilter || 75;
22
+
23
+ // Rules from consistent skip patterns
24
+ for (const pattern of patterns.consistentSkips || []) {
25
+ if (pattern.confidence >= minConfidence) {
26
+ rules.push({
27
+ id: `rule_${pattern.id}`,
28
+ type: 'skip_question',
29
+ questionId: pattern.questionId,
30
+ adjustment: -100, // Completely filter out
31
+ reason: `Consistently skipped (${pattern.confidence}% confidence, ${pattern.skipCount}/${pattern.sessionsAnalyzed} sessions)`,
32
+ confidence: pattern.confidence,
33
+ patternId: pattern.id,
34
+ appliedSince: new Date().toISOString(),
35
+ enabled: true
36
+ });
37
+ }
38
+ }
39
+
40
+ // Rules from category patterns
41
+ for (const pattern of patterns.categoryPatterns || []) {
42
+ if (pattern.confidence >= minConfidence) {
43
+ // Reduce relevance but don't completely filter
44
+ rules.push({
45
+ id: `rule_${pattern.id}`,
46
+ type: 'reduce_relevance',
47
+ category: pattern.category,
48
+ adjustment: -30, // Reduce score by 30 points
49
+ reason: `Category skip rate: ${pattern.confidence}% (${pattern.skipCount}/${pattern.totalQuestions} questions)`,
50
+ confidence: pattern.confidence,
51
+ patternId: pattern.id,
52
+ appliedSince: new Date().toISOString(),
53
+ enabled: true
54
+ });
55
+ }
56
+ }
57
+
58
+ // Rules from framework patterns
59
+ for (const pattern of patterns.frameworkPatterns || []) {
60
+ if (pattern.confidence >= minConfidence) {
61
+ rules.push({
62
+ id: `rule_${pattern.id}`,
63
+ type: 'framework_skip',
64
+ framework: pattern.framework,
65
+ questionId: pattern.questionId,
66
+ adjustment: -50, // Strong reduction for framework-specific skips
67
+ reason: `${pattern.framework} users skip this (${pattern.confidence}% confidence)`,
68
+ confidence: pattern.confidence,
69
+ patternId: pattern.id,
70
+ appliedSince: new Date().toISOString(),
71
+ enabled: true
72
+ });
73
+ }
74
+ }
75
+
76
+ // Rules from user preferences (informational, lower priority)
77
+ for (const pattern of patterns.userPreferences || []) {
78
+ if (pattern.preference === 'brief_answers' && pattern.avgWordCount < 20) {
79
+ // User gives very brief answers to this category
80
+ rules.push({
81
+ id: `rule_${pattern.id}`,
82
+ type: 'preference_brief',
83
+ category: pattern.category,
84
+ adjustment: -15, // Slight reduction
85
+ reason: `User prefers brief answers (avg ${pattern.avgWordCount} words)`,
86
+ confidence: pattern.confidence,
87
+ patternId: pattern.id,
88
+ appliedSince: new Date().toISOString(),
89
+ enabled: true
90
+ });
91
+ }
92
+ }
93
+
94
+ return rules;
95
+ }
96
+
97
+ /**
98
+ * Apply learned rules to a question's relevance score
99
+ * @param {Object} question - Question object
100
+ * @param {Object} projectContext - Project context
101
+ * @param {Array} rules - Learned rules
102
+ * @returns {Object} Adjustment details
103
+ */
104
+ function applyLearnedRules(question, projectContext, rules) {
105
+ let totalAdjustment = 0;
106
+ const appliedRules = [];
107
+
108
+ for (const rule of rules) {
109
+ if (!rule.enabled) continue;
110
+
111
+ let applies = false;
112
+
113
+ switch (rule.type) {
114
+ case 'skip_question':
115
+ if (question.id === rule.questionId) {
116
+ applies = true;
117
+ }
118
+ break;
119
+
120
+ case 'reduce_relevance':
121
+ if (question.category === rule.category) {
122
+ applies = true;
123
+ }
124
+ break;
125
+
126
+ case 'framework_skip':
127
+ if (
128
+ question.id === rule.questionId &&
129
+ projectContext.frameworks &&
130
+ projectContext.frameworks.includes(rule.framework)
131
+ ) {
132
+ applies = true;
133
+ }
134
+ break;
135
+
136
+ case 'preference_brief':
137
+ if (question.category === rule.category) {
138
+ applies = true;
139
+ }
140
+ break;
141
+ }
142
+
143
+ if (applies) {
144
+ totalAdjustment += rule.adjustment;
145
+ appliedRules.push({
146
+ ruleId: rule.id,
147
+ type: rule.type,
148
+ adjustment: rule.adjustment,
149
+ reason: rule.reason,
150
+ confidence: rule.confidence
151
+ });
152
+ }
153
+ }
154
+
155
+ return {
156
+ adjustment: totalAdjustment,
157
+ appliedRules,
158
+ hasLearning: appliedRules.length > 0
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Get rule explanations for user display
164
+ * @param {Array} rules - Learned rules
165
+ * @returns {Array} Formatted rule explanations
166
+ */
167
+ function getRuleExplanations(rules) {
168
+ const explanations = [];
169
+
170
+ // Group by type
171
+ const byType = {
172
+ skip_question: [],
173
+ reduce_relevance: [],
174
+ framework_skip: [],
175
+ preference_brief: []
176
+ };
177
+
178
+ for (const rule of rules) {
179
+ if (rule.enabled && byType[rule.type]) {
180
+ byType[rule.type].push(rule);
181
+ }
182
+ }
183
+
184
+ // Skip question rules
185
+ if (byType.skip_question.length > 0) {
186
+ explanations.push({
187
+ type: 'skip_question',
188
+ count: byType.skip_question.length,
189
+ message: `Skip ${byType.skip_question.length} question${byType.skip_question.length > 1 ? 's' : ''} (consistently skipped)`,
190
+ details: byType.skip_question.map(r => r.reason)
191
+ });
192
+ }
193
+
194
+ // Category reduction rules
195
+ if (byType.reduce_relevance.length > 0) {
196
+ const categories = byType.reduce_relevance.map(r => r.category).join(', ');
197
+ explanations.push({
198
+ type: 'reduce_relevance',
199
+ count: byType.reduce_relevance.length,
200
+ message: `Reduce relevance for: ${categories}`,
201
+ details: byType.reduce_relevance.map(r => r.reason)
202
+ });
203
+ }
204
+
205
+ // Framework skip rules
206
+ if (byType.framework_skip.length > 0) {
207
+ explanations.push({
208
+ type: 'framework_skip',
209
+ count: byType.framework_skip.length,
210
+ message: `Framework-specific filtering (${byType.framework_skip.length} rule${byType.framework_skip.length > 1 ? 's' : ''})`,
211
+ details: byType.framework_skip.map(r => r.reason)
212
+ });
213
+ }
214
+
215
+ // Preference rules
216
+ if (byType.preference_brief.length > 0) {
217
+ explanations.push({
218
+ type: 'preference_brief',
219
+ count: byType.preference_brief.length,
220
+ message: `User preference adjustments (${byType.preference_brief.length} categor${byType.preference_brief.length > 1 ? 'ies' : 'y'})`,
221
+ details: byType.preference_brief.map(r => r.reason)
222
+ });
223
+ }
224
+
225
+ return explanations;
226
+ }
227
+
228
+ /**
229
+ * Update learned rules based on new patterns
230
+ * @param {string} projectPath - Project root path
231
+ * @param {Object} patterns - Newly detected patterns
232
+ */
233
+ async function updateLearnedRules(projectPath, patterns) {
234
+ const config = await storage.getLearningConfig(projectPath);
235
+ const currentRules = await storage.getLearnedRules(projectPath);
236
+
237
+ // Generate new rules from patterns
238
+ const newRules = generateRulesFromPatterns(patterns, config);
239
+
240
+ // Merge with existing rules (don't duplicate)
241
+ const existingRuleIds = new Set(currentRules.rules.map(r => r.id));
242
+ const rulesToAdd = newRules.filter(r => !existingRuleIds.has(r.id));
243
+
244
+ // Add new rules
245
+ currentRules.rules.push(...rulesToAdd);
246
+
247
+ // Save updated rules
248
+ await storage.saveLearnedRules(projectPath, currentRules);
249
+
250
+ return {
251
+ added: rulesToAdd.length,
252
+ total: currentRules.rules.length
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Get active learned rules
258
+ * @param {string} projectPath - Project root path
259
+ * @returns {Promise<Array>} Active rules
260
+ */
261
+ async function getActiveRules(projectPath) {
262
+ const rulesData = await storage.getLearnedRules(projectPath);
263
+ return rulesData.rules.filter(r => r.enabled);
264
+ }
265
+
266
+ /**
267
+ * Enable or disable a specific rule
268
+ * @param {string} projectPath - Project root path
269
+ * @param {string} ruleId - Rule ID
270
+ * @param {boolean} enabled - Enable/disable
271
+ */
272
+ async function toggleRule(projectPath, ruleId, enabled) {
273
+ const rulesData = await storage.getLearnedRules(projectPath);
274
+ const rule = rulesData.rules.find(r => r.id === ruleId);
275
+
276
+ if (rule) {
277
+ rule.enabled = enabled;
278
+ await storage.saveLearnedRules(projectPath, rulesData);
279
+ return true;
280
+ }
281
+
282
+ return false;
283
+ }
284
+
285
+ /**
286
+ * Remove a learned rule
287
+ * @param {string} projectPath - Project root path
288
+ * @param {string} ruleId - Rule ID
289
+ */
290
+ async function removeRule(projectPath, ruleId) {
291
+ const rulesData = await storage.getLearnedRules(projectPath);
292
+ rulesData.rules = rulesData.rules.filter(r => r.id !== ruleId);
293
+ await storage.saveLearnedRules(projectPath, rulesData);
294
+ }
295
+
296
+ module.exports = {
297
+ generateRulesFromPatterns,
298
+ applyLearnedRules,
299
+ getRuleExplanations,
300
+ updateLearnedRules,
301
+ getActiveRules,
302
+ toggleRule,
303
+ removeRule
304
+ };