@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.
- package/.claude/settings.local.json +18 -0
- package/.project/PROJECT-SETTINGS.md +68 -0
- package/.project/chats/current/2025-10-05_UX-IMPROVEMENTS-AND-AI-ANALYSIS-CONFIG.md +389 -0
- package/.project/chats/current/SESSION-STATUS.md +205 -228
- package/.project/docs/DOCUMENTATION-UPDATE-CHECKLIST.md +196 -0
- package/.project/docs/ROADMAP.md +142 -44
- package/.project/docs/designs/LEARNING-ANALYTICS-DASHBOARD.md +1383 -0
- package/.project/docs/designs/PATTERN-DECAY-ALGORITHM.md +526 -0
- package/CHANGELOG.md +683 -0
- package/README.md +119 -24
- package/lib/learning/analytics-exporter.js +241 -0
- package/lib/learning/analytics-view.js +508 -0
- package/lib/learning/analytics.js +681 -0
- package/lib/learning/decay-manager.js +336 -0
- package/lib/learning/learning-manager.js +19 -6
- package/lib/learning/pattern-detector.js +285 -2
- package/lib/learning/storage.js +49 -1
- package/lib/utils/pre-publish-check.js +74 -0
- package/package.json +3 -2
- package/scripts/generate-test-data.js +557 -0
- package/tests/analytics-exporter.test.js +477 -0
- package/tests/analytics-view.test.js +466 -0
- package/tests/analytics.test.js +712 -0
- package/tests/decay-manager.test.js +394 -0
- package/tests/pattern-decay.test.js +339 -0
- /package/.project/chats/{current → complete}/2025-10-05_INTELLIGENT-ANSWER-ANALYSIS.md +0 -0
- /package/.project/chats/{current → complete}/2025-10-05_MULTI-IDE-IMPROVEMENTS.md +0 -0
|
@@ -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.
|
|
74
|
-
console.log(chalk.cyan(`│ 5.
|
|
75
|
-
console.log(chalk.cyan(`│ 6.
|
|
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.
|
|
89
|
-
{ name: '5.
|
|
90
|
-
{ name: '6.
|
|
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
|
-
|
|
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
|
};
|