@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,394 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const DecayManager = require('../lib/learning/decay-manager');
|
|
4
|
+
const storage = require('../lib/learning/storage');
|
|
5
|
+
|
|
6
|
+
describe('DecayManager Integration Tests', () => {
|
|
7
|
+
const testProjectPath = path.join(__dirname, 'test-project-decay');
|
|
8
|
+
let decayManager;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
// Clean up test directory
|
|
12
|
+
await fs.remove(testProjectPath);
|
|
13
|
+
await fs.ensureDir(testProjectPath);
|
|
14
|
+
|
|
15
|
+
decayManager = new DecayManager(testProjectPath);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
// Clean up
|
|
20
|
+
await fs.remove(testProjectPath);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createTestPattern = (overrides = {}) => ({
|
|
24
|
+
id: `pattern_${Date.now()}`,
|
|
25
|
+
type: 'consistent_skip',
|
|
26
|
+
questionId: 'q_deployment',
|
|
27
|
+
confidence: 90,
|
|
28
|
+
initialConfidence: 90,
|
|
29
|
+
sessionsAnalyzed: 10,
|
|
30
|
+
skipCount: 9,
|
|
31
|
+
status: 'active',
|
|
32
|
+
userApproved: false,
|
|
33
|
+
createdAt: new Date('2025-01-01').toISOString(),
|
|
34
|
+
lastSeen: new Date('2025-09-01').toISOString(),
|
|
35
|
+
lastDecayCalculation: new Date('2025-09-01').toISOString(),
|
|
36
|
+
timesRenewed: 0,
|
|
37
|
+
...overrides
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('loadPatternsWithDecay', () => {
|
|
41
|
+
test('loads patterns and applies decay', async () => {
|
|
42
|
+
// Create patterns with old lastSeen dates
|
|
43
|
+
const patterns = [
|
|
44
|
+
createTestPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-07-01').toISOString() }),
|
|
45
|
+
createTestPattern({ id: 'p2', confidence: 80, lastSeen: new Date('2025-07-01').toISOString() })
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
await storage.savePatterns(testProjectPath, {
|
|
49
|
+
version: '1.0',
|
|
50
|
+
patterns
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const loadedPatterns = await decayManager.loadPatternsWithDecay();
|
|
54
|
+
|
|
55
|
+
expect(loadedPatterns.patterns.length).toBe(2);
|
|
56
|
+
expect(loadedPatterns.patterns[0].confidence).toBeLessThan(90);
|
|
57
|
+
expect(loadedPatterns.patterns[1].confidence).toBeLessThan(80);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('removes stale patterns automatically', async () => {
|
|
61
|
+
// Create patterns - one stale, one active
|
|
62
|
+
const patterns = [
|
|
63
|
+
createTestPattern({
|
|
64
|
+
id: 'stale',
|
|
65
|
+
confidence: 35, // Below removeBelow threshold (40)
|
|
66
|
+
lastSeen: new Date('2025-09-01').toISOString()
|
|
67
|
+
}),
|
|
68
|
+
createTestPattern({
|
|
69
|
+
id: 'active',
|
|
70
|
+
confidence: 80,
|
|
71
|
+
lastSeen: new Date('2025-09-01').toISOString()
|
|
72
|
+
})
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
await storage.savePatterns(testProjectPath, {
|
|
76
|
+
version: '1.0',
|
|
77
|
+
patterns
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const loadedPatterns = await decayManager.loadPatternsWithDecay();
|
|
81
|
+
|
|
82
|
+
// Should only have active pattern
|
|
83
|
+
expect(loadedPatterns.patterns.length).toBe(1);
|
|
84
|
+
expect(loadedPatterns.patterns[0].id).toBe('active');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('skips decay if disabled in config', async () => {
|
|
88
|
+
// Set decay disabled
|
|
89
|
+
const config = await storage.getLearningConfig(testProjectPath);
|
|
90
|
+
config.decay.enabled = false;
|
|
91
|
+
await storage.saveLearningConfig(testProjectPath, config);
|
|
92
|
+
|
|
93
|
+
const patterns = [
|
|
94
|
+
createTestPattern({ confidence: 90, lastSeen: new Date('2025-06-01').toISOString() })
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
await storage.savePatterns(testProjectPath, {
|
|
98
|
+
version: '1.0',
|
|
99
|
+
patterns
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const loadedPatterns = await decayManager.loadPatternsWithDecay();
|
|
103
|
+
|
|
104
|
+
// Confidence should remain unchanged
|
|
105
|
+
expect(loadedPatterns.patterns[0].confidence).toBe(90);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('handles empty pattern list', async () => {
|
|
109
|
+
await storage.savePatterns(testProjectPath, {
|
|
110
|
+
version: '1.0',
|
|
111
|
+
patterns: []
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const loadedPatterns = await decayManager.loadPatternsWithDecay();
|
|
115
|
+
|
|
116
|
+
expect(loadedPatterns.patterns).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('checkForPatternRenewal', () => {
|
|
121
|
+
test('renews matching pattern on skip event', async () => {
|
|
122
|
+
const pattern = createTestPattern({
|
|
123
|
+
id: 'renewable',
|
|
124
|
+
questionId: 'q_deployment',
|
|
125
|
+
confidence: 60
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await storage.savePatterns(testProjectPath, {
|
|
129
|
+
version: '1.0',
|
|
130
|
+
patterns: [pattern]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const skipEvent = {
|
|
134
|
+
questionId: 'q_deployment',
|
|
135
|
+
action: 'skipped',
|
|
136
|
+
reason: 'manual'
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const renewedIds = await decayManager.checkForPatternRenewal(skipEvent);
|
|
140
|
+
|
|
141
|
+
expect(renewedIds.length).toBe(1);
|
|
142
|
+
|
|
143
|
+
// Verify pattern was renewed
|
|
144
|
+
const updatedPatterns = await storage.getPatterns(testProjectPath);
|
|
145
|
+
expect(updatedPatterns.patterns[0].confidence).toBe(70); // 60 + 10
|
|
146
|
+
expect(updatedPatterns.patterns[0].timesRenewed).toBe(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('renews category pattern on matching skip', async () => {
|
|
150
|
+
const pattern = createTestPattern({
|
|
151
|
+
id: 'category_pattern',
|
|
152
|
+
type: 'category_skip',
|
|
153
|
+
category: 'deployment',
|
|
154
|
+
confidence: 65
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await storage.savePatterns(testProjectPath, {
|
|
158
|
+
version: '1.0',
|
|
159
|
+
patterns: [pattern]
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const skipEvent = {
|
|
163
|
+
questionId: 'q_any',
|
|
164
|
+
category: 'deployment',
|
|
165
|
+
action: 'skipped',
|
|
166
|
+
reason: 'manual'
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const renewedIds = await decayManager.checkForPatternRenewal(skipEvent);
|
|
170
|
+
|
|
171
|
+
expect(renewedIds.length).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('limits renewals per day per pattern', async () => {
|
|
175
|
+
const pattern = createTestPattern({
|
|
176
|
+
id: 'limited',
|
|
177
|
+
questionId: 'q_test',
|
|
178
|
+
confidence: 60
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await storage.savePatterns(testProjectPath, {
|
|
182
|
+
version: '1.0',
|
|
183
|
+
patterns: [pattern]
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const skipEvent = {
|
|
187
|
+
questionId: 'q_test',
|
|
188
|
+
action: 'skipped',
|
|
189
|
+
reason: 'manual'
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// First renewal should work
|
|
193
|
+
let renewedIds = await decayManager.checkForPatternRenewal(skipEvent);
|
|
194
|
+
expect(renewedIds.length).toBe(1);
|
|
195
|
+
|
|
196
|
+
// Second renewal on same day should be blocked (maxRenewalsPerDay = 1)
|
|
197
|
+
renewedIds = await decayManager.checkForPatternRenewal(skipEvent);
|
|
198
|
+
expect(renewedIds.length).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('does not renew non-matching patterns', async () => {
|
|
202
|
+
const pattern = createTestPattern({
|
|
203
|
+
id: 'non_matching',
|
|
204
|
+
questionId: 'q_deployment',
|
|
205
|
+
confidence: 60
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await storage.savePatterns(testProjectPath, {
|
|
209
|
+
version: '1.0',
|
|
210
|
+
patterns: [pattern]
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const skipEvent = {
|
|
214
|
+
questionId: 'q_other_question',
|
|
215
|
+
action: 'skipped',
|
|
216
|
+
reason: 'manual'
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const renewedIds = await decayManager.checkForPatternRenewal(skipEvent);
|
|
220
|
+
|
|
221
|
+
expect(renewedIds.length).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('skips renewal if decay disabled', async () => {
|
|
225
|
+
// Disable decay
|
|
226
|
+
const config = await storage.getLearningConfig(testProjectPath);
|
|
227
|
+
config.decay.enabled = false;
|
|
228
|
+
await storage.saveLearningConfig(testProjectPath, config);
|
|
229
|
+
|
|
230
|
+
const pattern = createTestPattern({
|
|
231
|
+
questionId: 'q_test',
|
|
232
|
+
confidence: 60
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await storage.savePatterns(testProjectPath, {
|
|
236
|
+
version: '1.0',
|
|
237
|
+
patterns: [pattern]
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const skipEvent = {
|
|
241
|
+
questionId: 'q_test',
|
|
242
|
+
action: 'skipped'
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const renewedIds = await decayManager.checkForPatternRenewal(skipEvent);
|
|
246
|
+
|
|
247
|
+
expect(renewedIds.length).toBe(0);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('cleanupStalePatterns', () => {
|
|
252
|
+
test('removes patterns below confidence threshold', () => {
|
|
253
|
+
const patterns = [
|
|
254
|
+
createTestPattern({ id: 'low', confidence: 30 }), // Below 40
|
|
255
|
+
createTestPattern({ id: 'ok', confidence: 50 })
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns);
|
|
259
|
+
|
|
260
|
+
expect(activePatterns.length).toBe(1);
|
|
261
|
+
expect(activePatterns[0].id).toBe('ok');
|
|
262
|
+
expect(removedPatterns.length).toBe(1);
|
|
263
|
+
expect(removedPatterns[0].id).toBe('low');
|
|
264
|
+
expect(removedPatterns[0].removalReason).toBe('confidence_too_low');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('removes patterns inactive too long', () => {
|
|
268
|
+
const patterns = [
|
|
269
|
+
createTestPattern({
|
|
270
|
+
id: 'ancient',
|
|
271
|
+
confidence: 80,
|
|
272
|
+
lastSeen: new Date('2024-12-01').toISOString() // 10 months ago
|
|
273
|
+
}),
|
|
274
|
+
createTestPattern({
|
|
275
|
+
id: 'recent',
|
|
276
|
+
confidence: 80,
|
|
277
|
+
lastSeen: new Date('2025-09-01').toISOString()
|
|
278
|
+
})
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns);
|
|
282
|
+
|
|
283
|
+
expect(activePatterns.length).toBe(1);
|
|
284
|
+
expect(activePatterns[0].id).toBe('recent');
|
|
285
|
+
expect(removedPatterns.length).toBe(1);
|
|
286
|
+
expect(removedPatterns[0].id).toBe('ancient');
|
|
287
|
+
expect(removedPatterns[0].removalReason).toBe('inactive_too_long');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('keeps all patterns if none meet removal criteria', () => {
|
|
291
|
+
const patterns = [
|
|
292
|
+
createTestPattern({ confidence: 80, lastSeen: new Date('2025-09-01').toISOString() }),
|
|
293
|
+
createTestPattern({ confidence: 70, lastSeen: new Date('2025-09-01').toISOString() })
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns);
|
|
297
|
+
|
|
298
|
+
expect(activePatterns.length).toBe(2);
|
|
299
|
+
expect(removedPatterns.length).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('triggerDecayCalculation', () => {
|
|
304
|
+
test('manually triggers decay and returns stats', async () => {
|
|
305
|
+
const patterns = [
|
|
306
|
+
createTestPattern({ confidence: 90, lastSeen: new Date('2025-07-01').toISOString() }),
|
|
307
|
+
createTestPattern({ confidence: 35, lastSeen: new Date('2025-07-01').toISOString() }) // Will be removed
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
await storage.savePatterns(testProjectPath, {
|
|
311
|
+
version: '1.0',
|
|
312
|
+
patterns
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const results = await decayManager.triggerDecayCalculation();
|
|
316
|
+
|
|
317
|
+
expect(results.totalPatterns).toBe(2);
|
|
318
|
+
expect(results.activePatterns).toBe(1);
|
|
319
|
+
expect(results.removedPatterns).toBe(1);
|
|
320
|
+
expect(results.removed.length).toBe(1);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('handles empty pattern list', async () => {
|
|
324
|
+
await storage.savePatterns(testProjectPath, {
|
|
325
|
+
version: '1.0',
|
|
326
|
+
patterns: []
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const results = await decayManager.triggerDecayCalculation();
|
|
330
|
+
|
|
331
|
+
expect(results.totalPatterns).toBe(0);
|
|
332
|
+
expect(results.activePatterns).toBe(0);
|
|
333
|
+
expect(results.removedPatterns).toBe(0);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('getDecayStats', () => {
|
|
338
|
+
test('calculates decay statistics', async () => {
|
|
339
|
+
const patterns = [
|
|
340
|
+
createTestPattern({ confidence: 85, createdAt: new Date('2025-08-01').toISOString() }), // High
|
|
341
|
+
createTestPattern({ confidence: 70, createdAt: new Date('2025-08-01').toISOString() }), // Medium
|
|
342
|
+
createTestPattern({ confidence: 55, createdAt: new Date('2025-08-01').toISOString() }) // Low
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
await storage.savePatterns(testProjectPath, {
|
|
346
|
+
version: '1.0',
|
|
347
|
+
patterns
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const stats = await decayManager.getDecayStats();
|
|
351
|
+
|
|
352
|
+
expect(stats.totalPatterns).toBe(3);
|
|
353
|
+
expect(stats.highConfidence).toBe(1);
|
|
354
|
+
expect(stats.mediumConfidence).toBe(1);
|
|
355
|
+
expect(stats.lowConfidence).toBe(1);
|
|
356
|
+
expect(stats.avgConfidence).toBeGreaterThan(0);
|
|
357
|
+
expect(stats.avgAge).toBeGreaterThan(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('handles empty patterns', async () => {
|
|
361
|
+
await storage.savePatterns(testProjectPath, {
|
|
362
|
+
version: '1.0',
|
|
363
|
+
patterns: []
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const stats = await decayManager.getDecayStats();
|
|
367
|
+
|
|
368
|
+
expect(stats.totalPatterns).toBe(0);
|
|
369
|
+
expect(stats.highConfidence).toBe(0);
|
|
370
|
+
expect(stats.avgConfidence).toBe(0);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('saveRemovedPatternsHistory', () => {
|
|
375
|
+
test('saves removed patterns to history file', async () => {
|
|
376
|
+
const removedPatterns = [
|
|
377
|
+
createTestPattern({ id: 'removed1', confidence: 30 }),
|
|
378
|
+
createTestPattern({ id: 'removed2', confidence: 35 })
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
await decayManager.saveRemovedPatternsHistory(removedPatterns);
|
|
382
|
+
|
|
383
|
+
const historyFile = path.join(testProjectPath, '.adf', 'learning', 'removed-patterns.json');
|
|
384
|
+
const exists = await fs.pathExists(historyFile);
|
|
385
|
+
|
|
386
|
+
expect(exists).toBe(true);
|
|
387
|
+
|
|
388
|
+
const history = await fs.readJSON(historyFile);
|
|
389
|
+
expect(history.removals).toHaveLength(1);
|
|
390
|
+
expect(history.removals[0].removedCount).toBe(2);
|
|
391
|
+
expect(history.removals[0].patterns).toHaveLength(2);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|