@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,339 @@
|
|
|
1
|
+
const {
|
|
2
|
+
calculateDecay,
|
|
3
|
+
getDecayRate,
|
|
4
|
+
renewPattern,
|
|
5
|
+
applyDecayToStoredPatterns,
|
|
6
|
+
renewStoredPattern,
|
|
7
|
+
getDefaultDecayConfig
|
|
8
|
+
} = require('../lib/learning/pattern-detector');
|
|
9
|
+
|
|
10
|
+
describe('Pattern Decay Algorithm', () => {
|
|
11
|
+
const createPattern = (overrides = {}) => ({
|
|
12
|
+
id: 'pattern_001',
|
|
13
|
+
type: 'consistent_skip',
|
|
14
|
+
questionId: 'q_deployment',
|
|
15
|
+
confidence: 90,
|
|
16
|
+
initialConfidence: 90,
|
|
17
|
+
sessionsAnalyzed: 10,
|
|
18
|
+
skipCount: 9,
|
|
19
|
+
status: 'active',
|
|
20
|
+
userApproved: false,
|
|
21
|
+
createdAt: new Date('2025-01-01').toISOString(),
|
|
22
|
+
lastSeen: new Date('2025-09-01').toISOString(),
|
|
23
|
+
lastDecayCalculation: new Date('2025-09-01').toISOString(),
|
|
24
|
+
timesRenewed: 0,
|
|
25
|
+
...overrides
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const config = getDefaultDecayConfig();
|
|
29
|
+
|
|
30
|
+
describe('getDecayRate', () => {
|
|
31
|
+
test('high confidence (≥90) decays at half rate', () => {
|
|
32
|
+
const highRate = getDecayRate(95, 0.15);
|
|
33
|
+
expect(highRate).toBe(0.075); // 7.5%
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('medium confidence (75-89) decays at normal rate', () => {
|
|
37
|
+
const mediumRate = getDecayRate(80, 0.15);
|
|
38
|
+
expect(mediumRate).toBe(0.15); // 15%
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('low confidence (<75) decays at 1.5x rate', () => {
|
|
42
|
+
const lowRate = getDecayRate(65, 0.15);
|
|
43
|
+
expect(lowRate).toBe(0.225); // 22.5%
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('calculateDecay', () => {
|
|
48
|
+
test('decays confidence over time', () => {
|
|
49
|
+
const pattern = createPattern({
|
|
50
|
+
confidence: 90,
|
|
51
|
+
lastSeen: new Date('2025-08-01').toISOString()
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Mock current date to 2 months later
|
|
55
|
+
const mockDate = new Date('2025-10-01');
|
|
56
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
57
|
+
|
|
58
|
+
const decayed = calculateDecay(pattern, config);
|
|
59
|
+
|
|
60
|
+
// Should decay over 2 months
|
|
61
|
+
expect(decayed.confidence).toBeLessThan(90);
|
|
62
|
+
expect(decayed.lastDecayCalculation).toBe(mockDate.toISOString());
|
|
63
|
+
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('high confidence patterns decay slower than low confidence', () => {
|
|
68
|
+
const highPattern = createPattern({
|
|
69
|
+
confidence: 95,
|
|
70
|
+
lastSeen: new Date('2025-08-01').toISOString()
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const lowPattern = createPattern({
|
|
74
|
+
confidence: 65,
|
|
75
|
+
lastSeen: new Date('2025-08-01').toISOString()
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mockDate = new Date('2025-10-01');
|
|
79
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
80
|
+
|
|
81
|
+
const highDecayed = calculateDecay(highPattern, config);
|
|
82
|
+
const lowDecayed = calculateDecay(lowPattern, config);
|
|
83
|
+
|
|
84
|
+
// Calculate decay percentages
|
|
85
|
+
const highDecayPct = (highPattern.confidence - highDecayed.confidence) / highPattern.confidence;
|
|
86
|
+
const lowDecayPct = (lowPattern.confidence - lowDecayed.confidence) / lowPattern.confidence;
|
|
87
|
+
|
|
88
|
+
expect(highDecayPct).toBeLessThan(lowDecayPct);
|
|
89
|
+
|
|
90
|
+
jest.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('skips decay if disabled', () => {
|
|
94
|
+
const pattern = createPattern();
|
|
95
|
+
const disabledConfig = { ...config, enabled: false };
|
|
96
|
+
|
|
97
|
+
const result = calculateDecay(pattern, disabledConfig);
|
|
98
|
+
|
|
99
|
+
expect(result.confidence).toBe(pattern.confidence);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('skips decay for user-approved patterns when protected', () => {
|
|
103
|
+
const approvedPattern = createPattern({
|
|
104
|
+
userApproved: true,
|
|
105
|
+
lastSeen: new Date('2025-06-01').toISOString()
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const protectConfig = { ...config, protectApproved: true };
|
|
109
|
+
|
|
110
|
+
const mockDate = new Date('2025-10-01');
|
|
111
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
112
|
+
|
|
113
|
+
const decayed = calculateDecay(approvedPattern, protectConfig);
|
|
114
|
+
const normalDecayed = calculateDecay(createPattern({ userApproved: false, lastSeen: new Date('2025-06-01').toISOString() }), config);
|
|
115
|
+
|
|
116
|
+
// User-approved should decay slower
|
|
117
|
+
expect(decayed.confidence).toBeGreaterThan(normalDecayed.confidence);
|
|
118
|
+
|
|
119
|
+
jest.restoreAllMocks();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('skips decay if recently seen (< 1 week)', () => {
|
|
123
|
+
const recentPattern = createPattern({
|
|
124
|
+
lastSeen: new Date('2025-09-28').toISOString()
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const mockDate = new Date('2025-10-01');
|
|
128
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
129
|
+
|
|
130
|
+
const result = calculateDecay(recentPattern, config);
|
|
131
|
+
|
|
132
|
+
expect(result.confidence).toBe(recentPattern.confidence);
|
|
133
|
+
|
|
134
|
+
jest.restoreAllMocks();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('confidence never goes below 0', () => {
|
|
138
|
+
const lowPattern = createPattern({
|
|
139
|
+
confidence: 5,
|
|
140
|
+
lastSeen: new Date('2024-01-01').toISOString()
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const mockDate = new Date('2025-10-01');
|
|
144
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
145
|
+
|
|
146
|
+
const decayed = calculateDecay(lowPattern, config);
|
|
147
|
+
|
|
148
|
+
expect(decayed.confidence).toBeGreaterThanOrEqual(0);
|
|
149
|
+
|
|
150
|
+
jest.restoreAllMocks();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('renewPattern', () => {
|
|
155
|
+
test('increases confidence by renewal boost', () => {
|
|
156
|
+
const pattern = createPattern({ confidence: 60 });
|
|
157
|
+
|
|
158
|
+
const mockDate = new Date('2025-10-01');
|
|
159
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
160
|
+
|
|
161
|
+
const renewed = renewPattern(pattern, config);
|
|
162
|
+
|
|
163
|
+
expect(renewed.confidence).toBe(70); // 60 + 10 (renewalBoost)
|
|
164
|
+
expect(renewed.timesRenewed).toBe(1);
|
|
165
|
+
expect(renewed.lastSeen).toBe(mockDate.toISOString());
|
|
166
|
+
|
|
167
|
+
jest.restoreAllMocks();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('caps at initialConfidence + 5', () => {
|
|
171
|
+
const pattern = createPattern({
|
|
172
|
+
confidence: 93,
|
|
173
|
+
initialConfidence: 90
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const renewed = renewPattern(pattern, config);
|
|
177
|
+
|
|
178
|
+
expect(renewed.confidence).toBe(95); // Max is 90 + 5
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('never exceeds 100', () => {
|
|
182
|
+
const pattern = createPattern({
|
|
183
|
+
confidence: 96,
|
|
184
|
+
initialConfidence: 98
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const renewed = renewPattern(pattern, config);
|
|
188
|
+
|
|
189
|
+
expect(renewed.confidence).toBe(100); // Capped at 100
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('updates lastSeen and prevents immediate decay', () => {
|
|
193
|
+
const pattern = createPattern();
|
|
194
|
+
|
|
195
|
+
const mockDate = new Date('2025-10-01');
|
|
196
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
197
|
+
|
|
198
|
+
const renewed = renewPattern(pattern, config);
|
|
199
|
+
|
|
200
|
+
expect(renewed.lastSeen).toBe(mockDate.toISOString());
|
|
201
|
+
expect(renewed.lastDecayCalculation).toBe(mockDate.toISOString());
|
|
202
|
+
|
|
203
|
+
jest.restoreAllMocks();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('applyDecayToStoredPatterns', () => {
|
|
208
|
+
test('applies decay to all patterns', () => {
|
|
209
|
+
const patterns = [
|
|
210
|
+
createPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-08-01').toISOString() }),
|
|
211
|
+
createPattern({ id: 'p2', confidence: 80, lastSeen: new Date('2025-08-01').toISOString() }),
|
|
212
|
+
createPattern({ id: 'p3', confidence: 70, lastSeen: new Date('2025-08-01').toISOString() })
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const mockDate = new Date('2025-10-01');
|
|
216
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
217
|
+
|
|
218
|
+
const decayed = applyDecayToStoredPatterns(patterns, config);
|
|
219
|
+
|
|
220
|
+
expect(decayed.length).toBe(3);
|
|
221
|
+
expect(decayed[0].confidence).toBeLessThan(90);
|
|
222
|
+
expect(decayed[1].confidence).toBeLessThan(80);
|
|
223
|
+
expect(decayed[2].confidence).toBeLessThan(70);
|
|
224
|
+
|
|
225
|
+
jest.restoreAllMocks();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('handles empty pattern array', () => {
|
|
229
|
+
const patterns = [];
|
|
230
|
+
const decayed = applyDecayToStoredPatterns(patterns, config);
|
|
231
|
+
|
|
232
|
+
expect(decayed).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('renewStoredPattern', () => {
|
|
237
|
+
test('renews a single pattern', () => {
|
|
238
|
+
const pattern = createPattern({ confidence: 65 });
|
|
239
|
+
|
|
240
|
+
const mockDate = new Date('2025-10-01');
|
|
241
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
242
|
+
|
|
243
|
+
const renewed = renewStoredPattern(pattern, config);
|
|
244
|
+
|
|
245
|
+
expect(renewed.confidence).toBe(75);
|
|
246
|
+
expect(renewed.timesRenewed).toBe(1);
|
|
247
|
+
|
|
248
|
+
jest.restoreAllMocks();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('Edge Cases', () => {
|
|
253
|
+
test('handles very old patterns (6+ months)', () => {
|
|
254
|
+
const oldPattern = createPattern({
|
|
255
|
+
confidence: 80,
|
|
256
|
+
lastSeen: new Date('2024-12-01').toISOString()
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const mockDate = new Date('2025-10-01');
|
|
260
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
261
|
+
|
|
262
|
+
const decayed = calculateDecay(oldPattern, config);
|
|
263
|
+
|
|
264
|
+
// Should have significantly decayed after 10 months
|
|
265
|
+
expect(decayed.confidence).toBeLessThan(40);
|
|
266
|
+
|
|
267
|
+
jest.restoreAllMocks();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('handles pattern oscillation (multiple renewals)', () => {
|
|
271
|
+
let pattern = createPattern({ confidence: 70, timesRenewed: 0 });
|
|
272
|
+
|
|
273
|
+
// Simulate multiple renewals
|
|
274
|
+
for (let i = 0; i < 3; i++) {
|
|
275
|
+
pattern = renewPattern(pattern, config);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
expect(pattern.timesRenewed).toBe(3);
|
|
279
|
+
expect(pattern.confidence).toBeGreaterThan(70);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('handles missing timestamp fields gracefully', () => {
|
|
283
|
+
const incompletePattern = {
|
|
284
|
+
id: 'pattern_incomplete',
|
|
285
|
+
confidence: 80,
|
|
286
|
+
type: 'consistent_skip'
|
|
287
|
+
// Missing lastSeen, createdAt, etc.
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Should not throw error
|
|
291
|
+
expect(() => {
|
|
292
|
+
calculateDecay(incompletePattern, config);
|
|
293
|
+
}).not.toThrow();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('handles zero confidence gracefully', () => {
|
|
297
|
+
const zeroPattern = createPattern({ confidence: 0 });
|
|
298
|
+
const decayed = calculateDecay(zeroPattern, config);
|
|
299
|
+
|
|
300
|
+
expect(decayed.confidence).toBe(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('handles 100% confidence', () => {
|
|
304
|
+
const maxPattern = createPattern({
|
|
305
|
+
confidence: 100,
|
|
306
|
+
lastSeen: new Date('2025-08-01').toISOString()
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const mockDate = new Date('2025-10-01');
|
|
310
|
+
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
311
|
+
|
|
312
|
+
const decayed = calculateDecay(maxPattern, config);
|
|
313
|
+
|
|
314
|
+
expect(decayed.confidence).toBeLessThan(100);
|
|
315
|
+
expect(decayed.confidence).toBeGreaterThan(0);
|
|
316
|
+
|
|
317
|
+
jest.restoreAllMocks();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('getDefaultDecayConfig', () => {
|
|
322
|
+
test('returns valid default configuration', () => {
|
|
323
|
+
const defaultConfig = getDefaultDecayConfig();
|
|
324
|
+
|
|
325
|
+
expect(defaultConfig).toHaveProperty('enabled');
|
|
326
|
+
expect(defaultConfig).toHaveProperty('baseDecayRate');
|
|
327
|
+
expect(defaultConfig).toHaveProperty('minConfidenceThreshold');
|
|
328
|
+
expect(defaultConfig).toHaveProperty('removeBelow');
|
|
329
|
+
expect(defaultConfig).toHaveProperty('renewalBoost');
|
|
330
|
+
expect(defaultConfig).toHaveProperty('protectApproved');
|
|
331
|
+
expect(defaultConfig).toHaveProperty('maxInactiveMonths');
|
|
332
|
+
|
|
333
|
+
expect(defaultConfig.enabled).toBe(true);
|
|
334
|
+
expect(defaultConfig.baseDecayRate).toBe(0.15);
|
|
335
|
+
expect(defaultConfig.minConfidenceThreshold).toBe(50);
|
|
336
|
+
expect(defaultConfig.removeBelow).toBe(40);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
File without changes
|
|
File without changes
|