@iservu-inc/adf-cli 0.9.1 → 0.11.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.
@@ -0,0 +1,336 @@
1
+ const storage = require('./storage');
2
+ const {
3
+ applyDecayToStoredPatterns,
4
+ renewStoredPattern,
5
+ getDefaultDecayConfig
6
+ } = require('./pattern-detector');
7
+
8
+ /**
9
+ * Decay Manager - Orchestrates pattern decay operations
10
+ *
11
+ * Responsibilities:
12
+ * - Apply decay when loading patterns
13
+ * - Handle pattern renewal on skip events
14
+ * - Clean up stale patterns
15
+ * - Track decay history
16
+ */
17
+
18
+ class DecayManager {
19
+ constructor(projectPath) {
20
+ this.projectPath = projectPath;
21
+ this.decayConfig = null;
22
+ this.renewalTracking = {}; // Track renewals per day per pattern
23
+ }
24
+
25
+ /**
26
+ * Initialize decay manager with configuration
27
+ */
28
+ async initialize() {
29
+ this.decayConfig = await storage.getDecayConfig(this.projectPath);
30
+ }
31
+
32
+ /**
33
+ * Load patterns with decay applied
34
+ * @returns {Promise<Object>} Patterns with decay applied
35
+ */
36
+ async loadPatternsWithDecay() {
37
+ // Ensure config is loaded
38
+ if (!this.decayConfig) {
39
+ await this.initialize();
40
+ }
41
+
42
+ // Skip if decay disabled
43
+ if (!this.decayConfig.enabled) {
44
+ return await storage.getPatterns(this.projectPath);
45
+ }
46
+
47
+ // Load stored patterns
48
+ const storedPatterns = await storage.getPatterns(this.projectPath);
49
+
50
+ if (!storedPatterns || !storedPatterns.patterns || storedPatterns.patterns.length === 0) {
51
+ return storedPatterns;
52
+ }
53
+
54
+ // Apply decay to all patterns
55
+ const decayedPatterns = applyDecayToStoredPatterns(
56
+ storedPatterns.patterns,
57
+ this.decayConfig
58
+ );
59
+
60
+ // Separate active and stale patterns
61
+ const { activePatterns, removedPatterns } = this.cleanupStalePatterns(decayedPatterns);
62
+
63
+ // Save updated patterns
64
+ await storage.savePatterns(this.projectPath, {
65
+ ...storedPatterns,
66
+ patterns: activePatterns,
67
+ lastDecayCheck: new Date().toISOString()
68
+ });
69
+
70
+ // Log removed patterns if any
71
+ if (removedPatterns.length > 0) {
72
+ console.log(`[Decay] Removed ${removedPatterns.length} stale patterns`);
73
+
74
+ // Save removed patterns to history
75
+ await this.saveRemovedPatternsHistory(removedPatterns);
76
+ }
77
+
78
+ return {
79
+ ...storedPatterns,
80
+ patterns: activePatterns
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Check if skip event matches any pattern and renew if needed
86
+ * @param {Object} skipEvent - Skip event from skip-tracker
87
+ * @returns {Promise<Array>} Renewed pattern IDs
88
+ */
89
+ async checkForPatternRenewal(skipEvent) {
90
+ // Ensure config is loaded
91
+ if (!this.decayConfig) {
92
+ await this.initialize();
93
+ }
94
+
95
+ // Skip if decay disabled
96
+ if (!this.decayConfig.enabled) {
97
+ return [];
98
+ }
99
+
100
+ // Load current patterns
101
+ const storedPatterns = await storage.getPatterns(this.projectPath);
102
+
103
+ if (!storedPatterns || !storedPatterns.patterns || storedPatterns.patterns.length === 0) {
104
+ return [];
105
+ }
106
+
107
+ const renewedPatternIds = [];
108
+ const today = new Date().toISOString().split('T')[0];
109
+
110
+ // Check each pattern for match
111
+ for (let i = 0; i < storedPatterns.patterns.length; i++) {
112
+ const pattern = storedPatterns.patterns[i];
113
+
114
+ // Check if skip event matches this pattern
115
+ if (this.skipMatchesPattern(skipEvent, pattern)) {
116
+ // Check renewal limit (max renewals per day)
117
+ const renewalKey = `${pattern.id}_${today}`;
118
+ const renewalsToday = this.renewalTracking[renewalKey] || 0;
119
+
120
+ if (renewalsToday < this.decayConfig.maxRenewalsPerDay) {
121
+ // Renew pattern
122
+ const renewedPattern = renewStoredPattern(pattern, this.decayConfig);
123
+ storedPatterns.patterns[i] = renewedPattern;
124
+ renewedPatternIds.push(pattern.id);
125
+
126
+ // Track renewal
127
+ this.renewalTracking[renewalKey] = renewalsToday + 1;
128
+ }
129
+ }
130
+ }
131
+
132
+ // Save updated patterns if any were renewed
133
+ if (renewedPatternIds.length > 0) {
134
+ await storage.savePatterns(this.projectPath, storedPatterns);
135
+ console.log(`[Decay] Renewed ${renewedPatternIds.length} patterns`);
136
+ }
137
+
138
+ return renewedPatternIds;
139
+ }
140
+
141
+ /**
142
+ * Check if skip event matches a pattern
143
+ * @param {Object} skipEvent - Skip event
144
+ * @param {Object} pattern - Pattern to match
145
+ * @returns {boolean} True if matches
146
+ */
147
+ skipMatchesPattern(skipEvent, pattern) {
148
+ switch (pattern.type) {
149
+ case 'consistent_skip':
150
+ return skipEvent.questionId === pattern.questionId;
151
+
152
+ case 'category_skip':
153
+ return skipEvent.category === pattern.category;
154
+
155
+ case 'framework_skip':
156
+ return skipEvent.framework === pattern.framework;
157
+
158
+ default:
159
+ return false;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Clean up stale patterns
165
+ * @param {Array} patterns - Patterns to check
166
+ * @returns {Object} { activePatterns, removedPatterns }
167
+ */
168
+ cleanupStalePatterns(patterns) {
169
+ // Use default config if not initialized
170
+ const config = this.decayConfig || {
171
+ removeBelow: 40,
172
+ maxInactiveMonths: 6
173
+ };
174
+
175
+ const activePatterns = [];
176
+ const removedPatterns = [];
177
+ const now = new Date();
178
+
179
+ for (const pattern of patterns) {
180
+ // Check confidence threshold
181
+ if (pattern.confidence < config.removeBelow) {
182
+ removedPatterns.push({
183
+ ...pattern,
184
+ status: 'removed',
185
+ removedAt: now.toISOString(),
186
+ removalReason: 'confidence_too_low'
187
+ });
188
+ continue;
189
+ }
190
+
191
+ // Check inactivity threshold
192
+ const lastSeen = new Date(pattern.lastSeen);
193
+ const monthsInactive = (now - lastSeen) / (1000 * 60 * 60 * 24 * 30);
194
+
195
+ if (monthsInactive >= config.maxInactiveMonths) {
196
+ removedPatterns.push({
197
+ ...pattern,
198
+ status: 'removed',
199
+ removedAt: now.toISOString(),
200
+ removalReason: 'inactive_too_long'
201
+ });
202
+ continue;
203
+ }
204
+
205
+ // Pattern is still active
206
+ activePatterns.push(pattern);
207
+ }
208
+
209
+ return { activePatterns, removedPatterns };
210
+ }
211
+
212
+ /**
213
+ * Save removed patterns to history
214
+ * @param {Array} removedPatterns - Patterns that were removed
215
+ */
216
+ async saveRemovedPatternsHistory(removedPatterns) {
217
+ try {
218
+ const historyEntry = {
219
+ timestamp: new Date().toISOString(),
220
+ removedCount: removedPatterns.length,
221
+ patterns: removedPatterns
222
+ };
223
+
224
+ await storage.appendToLearningHistory(
225
+ this.projectPath,
226
+ 'removed-patterns.json',
227
+ historyEntry,
228
+ 'removals'
229
+ );
230
+ } catch (error) {
231
+ console.warn(`Warning: Could not save removed patterns history: ${error.message}`);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Manually trigger decay calculation
237
+ * @returns {Promise<Object>} Decay results
238
+ */
239
+ async triggerDecayCalculation() {
240
+ await this.initialize();
241
+
242
+ const storedPatterns = await storage.getPatterns(this.projectPath);
243
+
244
+ if (!storedPatterns || !storedPatterns.patterns || storedPatterns.patterns.length === 0) {
245
+ return {
246
+ totalPatterns: 0,
247
+ activePatterns: 0,
248
+ removedPatterns: 0
249
+ };
250
+ }
251
+
252
+ const beforeCount = storedPatterns.patterns.length;
253
+
254
+ // Apply decay
255
+ const decayedPatterns = applyDecayToStoredPatterns(
256
+ storedPatterns.patterns,
257
+ this.decayConfig
258
+ );
259
+
260
+ // Clean up stale patterns
261
+ const { activePatterns, removedPatterns } = this.cleanupStalePatterns(decayedPatterns);
262
+
263
+ // Save results
264
+ await storage.savePatterns(this.projectPath, {
265
+ ...storedPatterns,
266
+ patterns: activePatterns,
267
+ lastDecayCheck: new Date().toISOString()
268
+ });
269
+
270
+ if (removedPatterns.length > 0) {
271
+ await this.saveRemovedPatternsHistory(removedPatterns);
272
+ }
273
+
274
+ return {
275
+ totalPatterns: beforeCount,
276
+ activePatterns: activePatterns.length,
277
+ removedPatterns: removedPatterns.length,
278
+ removed: removedPatterns
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Get decay statistics
284
+ * @returns {Promise<Object>} Decay statistics
285
+ */
286
+ async getDecayStats() {
287
+ const patterns = await this.loadPatternsWithDecay();
288
+
289
+ if (!patterns || !patterns.patterns || patterns.patterns.length === 0) {
290
+ return {
291
+ totalPatterns: 0,
292
+ highConfidence: 0,
293
+ mediumConfidence: 0,
294
+ lowConfidence: 0,
295
+ avgConfidence: 0,
296
+ avgAge: 0
297
+ };
298
+ }
299
+
300
+ const now = new Date();
301
+ let totalConfidence = 0;
302
+ let totalAge = 0;
303
+ let highConfidence = 0;
304
+ let mediumConfidence = 0;
305
+ let lowConfidence = 0;
306
+
307
+ for (const pattern of patterns.patterns) {
308
+ totalConfidence += pattern.confidence;
309
+
310
+ // Calculate age in days
311
+ const created = new Date(pattern.createdAt);
312
+ const ageDays = (now - created) / (1000 * 60 * 60 * 24);
313
+ totalAge += ageDays;
314
+
315
+ // Count by confidence
316
+ if (pattern.confidence >= 80) {
317
+ highConfidence++;
318
+ } else if (pattern.confidence >= 60) {
319
+ mediumConfidence++;
320
+ } else {
321
+ lowConfidence++;
322
+ }
323
+ }
324
+
325
+ return {
326
+ totalPatterns: patterns.patterns.length,
327
+ highConfidence,
328
+ mediumConfidence,
329
+ lowConfidence,
330
+ avgConfidence: Math.round(totalConfidence / patterns.patterns.length),
331
+ avgAge: Math.round(totalAge / patterns.patterns.length)
332
+ };
333
+ }
334
+ }
335
+
336
+ module.exports = DecayManager;
@@ -4,6 +4,7 @@ const storage = require('./storage');
4
4
  const { analyzeSkipPatterns } = require('./skip-tracker');
5
5
  const { detectPatterns, getPatternSummary } = require('./pattern-detector');
6
6
  const { getActiveRules, toggleRule, removeRule } = require('./rule-generator');
7
+ const { showAnalyticsDashboard } = require('./analytics-view');
7
8
 
8
9
  /**
9
10
  * Learning Manager - CLI interface for managing learning data
@@ -70,9 +71,10 @@ class LearningManager {
70
71
  console.log(chalk.cyan(`│ 1. View Skip History`));
71
72
  console.log(chalk.cyan(`│ 2. Review Detected Patterns`));
72
73
  console.log(chalk.cyan(`│ 3. Manage Learned Rules`));
73
- console.log(chalk.cyan(`│ 4. Learning Settings ${config.enabled ? chalk.green('(✓ Enabled)') : chalk.yellow('(○ Disabled)')}`));
74
- console.log(chalk.cyan(`│ 5. Clear Learning Data`));
75
- console.log(chalk.cyan(`│ 6. Back to Main Menu`));
74
+ console.log(chalk.cyan(`│ 4. ${chalk.green('📊 Analytics Dashboard')} ${chalk.gray('(NEW)')}`));
75
+ console.log(chalk.cyan(`│ 5. Learning Settings ${config.enabled ? chalk.green('(✓ Enabled)') : chalk.yellow('(○ Disabled)')}`));
76
+ console.log(chalk.cyan(`│ 6. Clear Learning Data`));
77
+ console.log(chalk.cyan(`│ 7. Back to Main Menu`));
76
78
  console.log(chalk.cyan('│'));
77
79
  console.log(chalk.cyan.bold('└─────────────────────────────────────────────────────┘\n'));
78
80
 
@@ -85,9 +87,10 @@ class LearningManager {
85
87
  { name: '1. View Skip History', value: 'history' },
86
88
  { name: '2. Review Detected Patterns', value: 'patterns' },
87
89
  { name: '3. Manage Learned Rules', value: 'rules' },
88
- { name: '4. Learning Settings', value: 'settings' },
89
- { name: '5. Clear Learning Data', value: 'clear' },
90
- { name: '6. Back to Main Menu', value: 'back' }
90
+ { name: chalk.green('4. 📊 Analytics Dashboard') + chalk.gray(' (NEW)'), value: 'analytics' },
91
+ { name: '5. Learning Settings', value: 'settings' },
92
+ { name: '6. Clear Learning Data', value: 'clear' },
93
+ { name: '7. Back to Main Menu', value: 'back' }
91
94
  ]
92
95
  }
93
96
  ]);
@@ -102,6 +105,9 @@ class LearningManager {
102
105
  case 'rules':
103
106
  await this.manageRules();
104
107
  break;
108
+ case 'analytics':
109
+ await this.showAnalytics();
110
+ break;
105
111
  case 'settings':
106
112
  await this.manageSettings();
107
113
  break;
@@ -398,6 +404,13 @@ class LearningManager {
398
404
  await this.pressEnterToContinue();
399
405
  }
400
406
 
407
+ /**
408
+ * Show analytics dashboard
409
+ */
410
+ async showAnalytics() {
411
+ await showAnalyticsDashboard(this.projectPath);
412
+ }
413
+
401
414
  /**
402
415
  * Clear all learning data
403
416
  */
@@ -27,12 +27,20 @@ class PatternDetector {
27
27
  * @returns {Object} All detected patterns
28
28
  */
29
29
  detectAllPatterns() {
30
- return {
30
+ const patterns = {
31
31
  consistentSkips: this.detectConsistentSkips(),
32
32
  categoryPatterns: this.detectCategoryPatterns(),
33
33
  frameworkPatterns: this.detectFrameworkPatterns(),
34
34
  userPreferences: this.detectUserPreferences()
35
35
  };
36
+
37
+ // Enhance all patterns with decay metadata
38
+ return {
39
+ consistentSkips: this.enhancePatternsWithMetadata(patterns.consistentSkips),
40
+ categoryPatterns: this.enhancePatternsWithMetadata(patterns.categoryPatterns),
41
+ frameworkPatterns: this.enhancePatternsWithMetadata(patterns.frameworkPatterns),
42
+ userPreferences: this.enhancePatternsWithMetadata(patterns.userPreferences)
43
+ };
36
44
  }
37
45
 
38
46
  /**
@@ -322,6 +330,197 @@ class PatternDetector {
322
330
 
323
331
  return frameworkData;
324
332
  }
333
+
334
+ /**
335
+ * Calculate decay for a pattern based on time inactive
336
+ * @param {Object} pattern - Pattern to apply decay to
337
+ * @param {Object} decayConfig - Decay configuration
338
+ * @returns {Object} Pattern with updated confidence
339
+ */
340
+ calculateDecay(pattern, decayConfig) {
341
+ // Skip if decay disabled
342
+ if (!decayConfig || !decayConfig.enabled) {
343
+ return pattern;
344
+ }
345
+
346
+ // Skip if user manually approved and protection enabled
347
+ if (pattern.userApproved && decayConfig.protectApproved) {
348
+ // Approved patterns decay at half rate
349
+ decayConfig = { ...decayConfig, baseDecayRate: decayConfig.baseDecayRate * 0.5 };
350
+ }
351
+
352
+ // Initialize timestamps if missing (backward compatibility)
353
+ const now = new Date();
354
+ const lastSeen = pattern.lastSeen ? new Date(pattern.lastSeen) : now;
355
+ const lastDecay = pattern.lastDecayCalculation ? new Date(pattern.lastDecayCalculation) : now;
356
+
357
+ // Calculate months since last seen
358
+ const monthsInactive = (now - lastSeen) / (1000 * 60 * 60 * 24 * 30);
359
+
360
+ // Skip if recently seen (< 1 week)
361
+ if (monthsInactive < 0.25) {
362
+ return pattern;
363
+ }
364
+
365
+ // Get decay rate based on confidence level
366
+ const decayRate = this.getDecayRate(pattern.confidence, decayConfig.baseDecayRate);
367
+
368
+ // Apply exponential decay
369
+ const newConfidence = pattern.confidence * Math.pow(1 - decayRate, monthsInactive);
370
+
371
+ // Round to 2 decimal places
372
+ const roundedConfidence = Math.round(newConfidence * 100) / 100;
373
+
374
+ // Ensure confidence doesn't go below 0
375
+ const finalConfidence = Math.max(0, roundedConfidence);
376
+
377
+ return {
378
+ ...pattern,
379
+ confidence: finalConfidence,
380
+ lastDecayCalculation: now.toISOString(),
381
+ // Store initial confidence if not already set
382
+ initialConfidence: pattern.initialConfidence || pattern.confidence
383
+ };
384
+ }
385
+
386
+ /**
387
+ * Get decay rate based on confidence level
388
+ * @param {number} confidence - Current confidence (0-100)
389
+ * @param {number} baseRate - Base decay rate (default 0.15)
390
+ * @returns {number} Adjusted decay rate
391
+ */
392
+ getDecayRate(confidence, baseRate = 0.15) {
393
+ // High confidence patterns decay slower
394
+ if (confidence >= 90) {
395
+ return baseRate * 0.5; // 7.5% per month for high confidence
396
+ }
397
+ // Medium confidence patterns decay normally
398
+ if (confidence >= 75) {
399
+ return baseRate; // 15% per month
400
+ }
401
+ // Low confidence patterns decay faster
402
+ return baseRate * 1.5; // 22.5% per month for low confidence
403
+ }
404
+
405
+ /**
406
+ * Renew a pattern when it's reconfirmed by user behavior
407
+ * @param {Object} pattern - Pattern to renew
408
+ * @param {Object} decayConfig - Decay configuration
409
+ * @returns {Object} Renewed pattern
410
+ */
411
+ renewPattern(pattern, decayConfig) {
412
+ const now = new Date();
413
+ const renewalBoost = decayConfig?.renewalBoost || 10;
414
+
415
+ // Calculate new confidence with boost
416
+ let newConfidence = pattern.confidence + renewalBoost;
417
+
418
+ // Cap at initial confidence + growth allowance
419
+ const maxConfidence = Math.min((pattern.initialConfidence || pattern.confidence) + 5, 100);
420
+ newConfidence = Math.min(newConfidence, maxConfidence);
421
+
422
+ return {
423
+ ...pattern,
424
+ confidence: newConfidence,
425
+ lastSeen: now.toISOString(),
426
+ lastDecayCalculation: now.toISOString(),
427
+ timesRenewed: (pattern.timesRenewed || 0) + 1
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Apply decay to all patterns
433
+ * @param {Array} patterns - Array of patterns
434
+ * @param {Object} decayConfig - Decay configuration
435
+ * @returns {Array} Patterns with decay applied
436
+ */
437
+ applyDecayToPatterns(patterns, decayConfig) {
438
+ if (!decayConfig || !decayConfig.enabled) {
439
+ return patterns;
440
+ }
441
+
442
+ return patterns.map(pattern => this.calculateDecay(pattern, decayConfig));
443
+ }
444
+
445
+ /**
446
+ * Cleanup stale patterns (remove those below threshold)
447
+ * @param {Array} patterns - Array of patterns
448
+ * @param {Object} decayConfig - Decay configuration
449
+ * @returns {Object} Active and removed patterns
450
+ */
451
+ cleanupStalePatterns(patterns, decayConfig) {
452
+ const now = new Date();
453
+ const activePatterns = [];
454
+ const removedPatterns = [];
455
+
456
+ const removeBelow = decayConfig?.removeBelow || 40;
457
+ const maxInactiveMonths = decayConfig?.maxInactiveMonths || 6;
458
+
459
+ for (const pattern of patterns) {
460
+ // Apply decay first
461
+ const decayedPattern = this.calculateDecay(pattern, decayConfig);
462
+
463
+ // Check if below removal threshold
464
+ if (decayedPattern.confidence < removeBelow) {
465
+ removedPatterns.push({
466
+ ...decayedPattern,
467
+ status: 'removed',
468
+ removedAt: now.toISOString(),
469
+ removalReason: 'confidence_too_low'
470
+ });
471
+ }
472
+ // Check if inactive for too long
473
+ else if (this.isInactiveTooLong(decayedPattern, maxInactiveMonths)) {
474
+ removedPatterns.push({
475
+ ...decayedPattern,
476
+ status: 'removed',
477
+ removedAt: now.toISOString(),
478
+ removalReason: 'inactive_too_long'
479
+ });
480
+ }
481
+ else {
482
+ activePatterns.push(decayedPattern);
483
+ }
484
+ }
485
+
486
+ return { activePatterns, removedPatterns };
487
+ }
488
+
489
+ /**
490
+ * Check if pattern has been inactive for too long
491
+ * @param {Object} pattern - Pattern to check
492
+ * @param {number} maxMonths - Maximum inactive months
493
+ * @returns {boolean} True if inactive too long
494
+ */
495
+ isInactiveTooLong(pattern, maxMonths = 6) {
496
+ if (!pattern.lastSeen) {
497
+ return false; // No data, assume active
498
+ }
499
+
500
+ const now = new Date();
501
+ const lastSeen = new Date(pattern.lastSeen);
502
+ const monthsInactive = (now - lastSeen) / (1000 * 60 * 60 * 24 * 30);
503
+
504
+ return monthsInactive >= maxMonths;
505
+ }
506
+
507
+ /**
508
+ * Enhance patterns with decay metadata
509
+ * @param {Array} patterns - Patterns to enhance
510
+ * @returns {Array} Enhanced patterns with timestamps
511
+ */
512
+ enhancePatternsWithMetadata(patterns) {
513
+ const now = new Date();
514
+
515
+ return patterns.map(pattern => ({
516
+ ...pattern,
517
+ createdAt: pattern.createdAt || now.toISOString(),
518
+ lastSeen: pattern.lastSeen || now.toISOString(),
519
+ lastDecayCalculation: pattern.lastDecayCalculation || now.toISOString(),
520
+ initialConfidence: pattern.initialConfidence || pattern.confidence,
521
+ timesRenewed: pattern.timesRenewed || 0
522
+ }));
523
+ }
325
524
  }
326
525
 
327
526
  /**
@@ -369,8 +568,92 @@ function getPatternSummary(patterns) {
369
568
  };
370
569
  }
371
570
 
571
+ /**
572
+ * Apply decay to patterns from storage
573
+ * @param {Array} patterns - Patterns to apply decay to
574
+ * @param {Object} decayConfig - Decay configuration
575
+ * @returns {Object} Active and removed patterns
576
+ */
577
+ function applyDecayToStoredPatterns(patterns, decayConfig) {
578
+ if (!patterns || patterns.length === 0) {
579
+ return [];
580
+ }
581
+
582
+ // Create a detector instance just for decay operations
583
+ const detector = new PatternDetector({ sessions: [] }, { sessions: [] }, {});
584
+
585
+ // Apply decay to all patterns (returns array)
586
+ return detector.applyDecayToPatterns(patterns, decayConfig);
587
+ }
588
+
589
+ /**
590
+ * Renew a pattern (boost confidence when reconfirmed)
591
+ * @param {Object} pattern - Pattern to renew
592
+ * @param {Object} decayConfig - Decay configuration
593
+ * @returns {Object} Renewed pattern
594
+ */
595
+ function renewStoredPattern(pattern, decayConfig) {
596
+ const detector = new PatternDetector({ sessions: [] }, { sessions: [] }, {});
597
+ return detector.renewPattern(pattern, decayConfig);
598
+ }
599
+
600
+ /**
601
+ * Get default decay configuration
602
+ * @returns {Object} Default decay config
603
+ */
604
+ function getDefaultDecayConfig() {
605
+ return {
606
+ enabled: true,
607
+ baseDecayRate: 0.15, // 15% per month
608
+ minConfidenceThreshold: 50, // Don't filter if confidence < 50
609
+ removeBelow: 40, // Auto-remove if < 40
610
+ renewalBoost: 10, // +10 points on renewal
611
+ protectApproved: true, // Approved patterns decay at 0.5x rate
612
+ maxInactiveMonths: 6 // Remove if inactive for 6+ months
613
+ };
614
+ }
615
+
616
+ /**
617
+ * Calculate decay for a pattern (standalone function for testing)
618
+ * @param {Object} pattern - Pattern object
619
+ * @param {Object} decayConfig - Decay configuration
620
+ * @returns {Object} Pattern with updated confidence
621
+ */
622
+ function calculateDecay(pattern, decayConfig) {
623
+ const detector = new PatternDetector({ sessions: [] }, { sessions: [] }, {});
624
+ return detector.calculateDecay(pattern, decayConfig);
625
+ }
626
+
627
+ /**
628
+ * Get decay rate based on confidence level (standalone function for testing)
629
+ * @param {number} confidence - Pattern confidence (0-100)
630
+ * @param {number} baseRate - Base decay rate (default 0.15)
631
+ * @returns {number} Adjusted decay rate
632
+ */
633
+ function getDecayRate(confidence, baseRate = 0.15) {
634
+ const detector = new PatternDetector({ sessions: [] }, { sessions: [] }, {});
635
+ return detector.getDecayRate(confidence, baseRate);
636
+ }
637
+
638
+ /**
639
+ * Renew pattern (standalone function for testing)
640
+ * @param {Object} pattern - Pattern object
641
+ * @param {Object} decayConfig - Decay configuration
642
+ * @returns {Object} Renewed pattern
643
+ */
644
+ function renewPattern(pattern, decayConfig) {
645
+ const detector = new PatternDetector({ sessions: [] }, { sessions: [] }, {});
646
+ return detector.renewPattern(pattern, decayConfig);
647
+ }
648
+
372
649
  module.exports = {
373
650
  PatternDetector,
374
651
  detectPatterns,
375
- getPatternSummary
652
+ getPatternSummary,
653
+ applyDecayToStoredPatterns,
654
+ renewStoredPattern,
655
+ getDefaultDecayConfig,
656
+ calculateDecay,
657
+ getDecayRate,
658
+ renewPattern
376
659
  };