@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,681 @@
|
|
|
1
|
+
const storage = require('./storage');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Learning Analytics Module
|
|
7
|
+
*
|
|
8
|
+
* Provides comprehensive analytics for the learning system including:
|
|
9
|
+
* - Overview statistics
|
|
10
|
+
* - Skip trends over time
|
|
11
|
+
* - Category preferences
|
|
12
|
+
* - Pattern confidence distribution
|
|
13
|
+
* - Learning effectiveness metrics
|
|
14
|
+
* - Pattern decay status
|
|
15
|
+
* - Session timeline analysis
|
|
16
|
+
* - Most impactful patterns
|
|
17
|
+
* - Question statistics
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate complete analytics report
|
|
22
|
+
* @param {string} projectPath - Project root path
|
|
23
|
+
* @returns {Promise<Object>} Complete analytics data
|
|
24
|
+
*/
|
|
25
|
+
async function generateAnalytics(projectPath) {
|
|
26
|
+
try {
|
|
27
|
+
// Load all learning data
|
|
28
|
+
const skipHistory = await storage.getSkipHistory(projectPath) || { sessions: [] };
|
|
29
|
+
const answerHistory = await storage.getAnswerHistory(projectPath) || { sessions: [] };
|
|
30
|
+
const patterns = await storage.getPatterns(projectPath) || { patterns: [] };
|
|
31
|
+
const rules = await storage.getLearnedRules(projectPath) || { rules: [] };
|
|
32
|
+
const stats = await storage.getLearningStats(projectPath) || {};
|
|
33
|
+
const config = await storage.getLearningConfig(projectPath) || {};
|
|
34
|
+
|
|
35
|
+
// Calculate all metrics
|
|
36
|
+
const overview = calculateOverviewStats(stats, patterns, rules, skipHistory, answerHistory);
|
|
37
|
+
const skipTrends = calculateSkipTrends(skipHistory);
|
|
38
|
+
const categoryPreferences = calculateCategoryPreferences(skipHistory, answerHistory);
|
|
39
|
+
const patternDistribution = calculatePatternDistribution(patterns.patterns);
|
|
40
|
+
const effectiveness = calculateEffectiveness(skipHistory, patterns.patterns, rules.rules);
|
|
41
|
+
const decayStatus = calculateDecayStatus(patterns.patterns, config);
|
|
42
|
+
const sessionTimeline = calculateSessionTimeline(skipHistory, answerHistory);
|
|
43
|
+
const impactfulPatterns = calculateImpactfulPatterns(patterns.patterns, rules.rules, skipHistory);
|
|
44
|
+
const questionStats = calculateQuestionStats(skipHistory, answerHistory);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
version: '1.0',
|
|
48
|
+
exportedAt: new Date().toISOString(),
|
|
49
|
+
exportType: 'learning-analytics',
|
|
50
|
+
dataRange: {
|
|
51
|
+
firstSession: skipHistory.sessions.length > 0 ? skipHistory.sessions[0].timestamp : null,
|
|
52
|
+
lastSession: skipHistory.sessions.length > 0 ? skipHistory.sessions[skipHistory.sessions.length - 1].timestamp : null,
|
|
53
|
+
totalSessions: skipHistory.sessions.length,
|
|
54
|
+
daysSpanned: calculateDaysSpanned(skipHistory.sessions)
|
|
55
|
+
},
|
|
56
|
+
overview,
|
|
57
|
+
skipTrends,
|
|
58
|
+
categoryPreferences,
|
|
59
|
+
patternDistribution,
|
|
60
|
+
effectiveness,
|
|
61
|
+
decayStatus,
|
|
62
|
+
sessionTimeline,
|
|
63
|
+
impactfulPatterns,
|
|
64
|
+
questionStats
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Error generating analytics:', error.message);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Calculate overview statistics
|
|
74
|
+
* @param {Object} stats - Stats object
|
|
75
|
+
* @param {Object} patterns - Patterns object
|
|
76
|
+
* @param {Object} rules - Rules object
|
|
77
|
+
* @param {Object} skipHistory - Skip history
|
|
78
|
+
* @param {Object} answerHistory - Answer history
|
|
79
|
+
* @returns {Object} Overview statistics
|
|
80
|
+
*/
|
|
81
|
+
function calculateOverviewStats(stats, patterns, rules, skipHistory, answerHistory) {
|
|
82
|
+
// Count all patterns except those explicitly marked as removed
|
|
83
|
+
const activePatterns = patterns.patterns.filter(p => !p.status || p.status !== 'removed').length;
|
|
84
|
+
const activeRules = rules.rules.filter(r => r.enabled !== false).length;
|
|
85
|
+
|
|
86
|
+
// Calculate learning system age
|
|
87
|
+
const firstSession = skipHistory.sessions.length > 0 ? skipHistory.sessions[0] : null;
|
|
88
|
+
const learningAge = firstSession ? daysBetween(new Date(firstSession.timestamp), new Date()) : 0;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
totalSessions: stats.totalSessions || skipHistory.sessions.length,
|
|
92
|
+
totalSkips: stats.totalSkips || countTotalSkips(skipHistory),
|
|
93
|
+
manualSkips: countManualSkips(skipHistory),
|
|
94
|
+
filteredSkips: countFilteredSkips(skipHistory),
|
|
95
|
+
totalAnswers: stats.totalAnswers || countTotalAnswers(answerHistory),
|
|
96
|
+
activePatterns,
|
|
97
|
+
activeRules,
|
|
98
|
+
learningAge,
|
|
99
|
+
dataSizeMB: 0 // Will be calculated if needed
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Calculate skip trends over last 12 weeks
|
|
105
|
+
* @param {Object} skipHistory - Skip history
|
|
106
|
+
* @returns {Array} Skip trends by week
|
|
107
|
+
*/
|
|
108
|
+
function calculateSkipTrends(skipHistory) {
|
|
109
|
+
const weeks = getLast12Weeks();
|
|
110
|
+
const trendData = weeks.map(week => {
|
|
111
|
+
const weekSessions = filterSessionsByWeek(skipHistory.sessions, week);
|
|
112
|
+
const manualSkips = countSkipsByReason(weekSessions, 'manual');
|
|
113
|
+
const filteredSkips = countSkipsByReason(weekSessions, 'filtered');
|
|
114
|
+
const totalAnswers = countAnswersInSessions(weekSessions);
|
|
115
|
+
const totalQuestions = manualSkips + filteredSkips + totalAnswers;
|
|
116
|
+
const skipRate = totalQuestions > 0 ? Math.round((manualSkips + filteredSkips) / totalQuestions * 100) : 0;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
week: week.label,
|
|
120
|
+
weekNumber: week.weekNumber,
|
|
121
|
+
manualSkips,
|
|
122
|
+
filteredSkips,
|
|
123
|
+
totalSkips: manualSkips + filteredSkips,
|
|
124
|
+
skipRate
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return trendData;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Calculate category preferences (skip rates by category)
|
|
133
|
+
* @param {Object} skipHistory - Skip history
|
|
134
|
+
* @param {Object} answerHistory - Answer history
|
|
135
|
+
* @returns {Array} Category preferences sorted by skip rate
|
|
136
|
+
*/
|
|
137
|
+
function calculateCategoryPreferences(skipHistory, answerHistory) {
|
|
138
|
+
const categories = {};
|
|
139
|
+
|
|
140
|
+
// Count skips by category
|
|
141
|
+
for (const session of skipHistory.sessions) {
|
|
142
|
+
for (const skip of session.skips || []) {
|
|
143
|
+
if (!skip.category) continue;
|
|
144
|
+
if (!categories[skip.category]) {
|
|
145
|
+
categories[skip.category] = { skips: 0, answers: 0 };
|
|
146
|
+
}
|
|
147
|
+
categories[skip.category].skips++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Count answers by category
|
|
152
|
+
for (const session of answerHistory.sessions) {
|
|
153
|
+
for (const answer of session.answers || []) {
|
|
154
|
+
if (!answer.category) continue;
|
|
155
|
+
if (!categories[answer.category]) {
|
|
156
|
+
categories[answer.category] = { skips: 0, answers: 0 };
|
|
157
|
+
}
|
|
158
|
+
categories[answer.category].answers++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Calculate skip rates
|
|
163
|
+
const preferences = Object.entries(categories).map(([category, data]) => {
|
|
164
|
+
const total = data.skips + data.answers;
|
|
165
|
+
const skipRate = total > 0 ? Math.round((data.skips / total) * 100) : 0;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
category,
|
|
169
|
+
skips: data.skips,
|
|
170
|
+
answers: data.answers,
|
|
171
|
+
total,
|
|
172
|
+
skipRate,
|
|
173
|
+
level: getSkipLevel(skipRate)
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Sort by skip rate descending
|
|
178
|
+
return preferences.sort((a, b) => b.skipRate - a.skipRate);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate pattern confidence distribution
|
|
183
|
+
* @param {Array} patterns - Array of patterns
|
|
184
|
+
* @returns {Object} Pattern distribution data
|
|
185
|
+
*/
|
|
186
|
+
function calculatePatternDistribution(patterns) {
|
|
187
|
+
const distribution = {
|
|
188
|
+
high: patterns.filter(p => p.confidence >= 90).length,
|
|
189
|
+
medium: patterns.filter(p => p.confidence >= 75 && p.confidence < 90).length,
|
|
190
|
+
low: patterns.filter(p => p.confidence >= 50 && p.confidence < 75).length,
|
|
191
|
+
veryLow: patterns.filter(p => p.confidence < 50).length
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const avgConfidence = patterns.length > 0
|
|
195
|
+
? Math.round(patterns.reduce((sum, p) => sum + p.confidence, 0) / patterns.length)
|
|
196
|
+
: 0;
|
|
197
|
+
|
|
198
|
+
const atRisk = patterns.filter(p => p.confidence < 50 || isInactive(p, 5));
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
distribution,
|
|
202
|
+
avgConfidence,
|
|
203
|
+
totalPatterns: patterns.length,
|
|
204
|
+
atRiskCount: atRisk.length,
|
|
205
|
+
atRiskPatterns: atRisk.map(p => ({
|
|
206
|
+
id: p.id,
|
|
207
|
+
questionId: p.questionId,
|
|
208
|
+
confidence: p.confidence,
|
|
209
|
+
lastSeen: p.lastSeen
|
|
210
|
+
}))
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Calculate learning effectiveness metrics
|
|
216
|
+
* @param {Object} skipHistory - Skip history
|
|
217
|
+
* @param {Array} patterns - Array of patterns
|
|
218
|
+
* @param {Array} rules - Array of rules
|
|
219
|
+
* @returns {Object} Effectiveness metrics
|
|
220
|
+
*/
|
|
221
|
+
function calculateEffectiveness(skipHistory, patterns, rules) {
|
|
222
|
+
// Time saved: filtered questions × avg time per question
|
|
223
|
+
const avgTimePerQuestion = 2.5; // minutes
|
|
224
|
+
const questionsFiltered = countFilteredQuestions(skipHistory);
|
|
225
|
+
const timeSavedMinutes = Math.round(questionsFiltered * avgTimePerQuestion);
|
|
226
|
+
|
|
227
|
+
// Pattern accuracy: user-approved patterns / total patterns
|
|
228
|
+
const approvedPatterns = patterns.filter(p => p.userApproved).length;
|
|
229
|
+
const patternAccuracy = patterns.length > 0
|
|
230
|
+
? Math.round((approvedPatterns / patterns.length) * 100)
|
|
231
|
+
: 0;
|
|
232
|
+
|
|
233
|
+
// Rule application rate
|
|
234
|
+
const totalRuleApplications = rules.reduce((sum, r) => sum + (r.appliedCount || 0), 0);
|
|
235
|
+
const avgApplicationsPerRule = rules.length > 0
|
|
236
|
+
? Math.round(totalRuleApplications / rules.length)
|
|
237
|
+
: 0;
|
|
238
|
+
|
|
239
|
+
// Overall effectiveness (simple: pattern accuracy)
|
|
240
|
+
const overallEffectiveness = patternAccuracy;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
timeSavedMinutes,
|
|
244
|
+
timeSavedHours: (timeSavedMinutes / 60).toFixed(1),
|
|
245
|
+
questionsFiltered,
|
|
246
|
+
falsePositives: 0, // TODO: Implement tracking
|
|
247
|
+
falsePositiveRate: 0,
|
|
248
|
+
patternAccuracy,
|
|
249
|
+
totalRuleApplications,
|
|
250
|
+
avgApplicationsPerRule,
|
|
251
|
+
overallEffectiveness
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Calculate pattern decay status
|
|
257
|
+
* @param {Array} patterns - Array of patterns
|
|
258
|
+
* @param {Object} config - Learning config
|
|
259
|
+
* @returns {Object} Decay status data
|
|
260
|
+
*/
|
|
261
|
+
function calculateDecayStatus(patterns, config) {
|
|
262
|
+
const now = new Date();
|
|
263
|
+
const decayStatus = {
|
|
264
|
+
healthy: [],
|
|
265
|
+
warning: [],
|
|
266
|
+
critical: []
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
for (const pattern of patterns) {
|
|
270
|
+
const daysSinceLastSeen = pattern.lastSeen
|
|
271
|
+
? daysBetween(new Date(pattern.lastSeen), now)
|
|
272
|
+
: 999;
|
|
273
|
+
const monthsInactive = daysSinceLastSeen / 30;
|
|
274
|
+
|
|
275
|
+
// Categorize by confidence and inactivity
|
|
276
|
+
if (pattern.confidence >= 75 && monthsInactive < 3) {
|
|
277
|
+
decayStatus.healthy.push(pattern);
|
|
278
|
+
} else if (pattern.confidence >= 50 && monthsInactive < 5) {
|
|
279
|
+
decayStatus.warning.push(pattern);
|
|
280
|
+
} else {
|
|
281
|
+
decayStatus.critical.push(pattern);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Recently renewed patterns (last 30 days)
|
|
286
|
+
const recentlyRenewed = patterns.filter(p => {
|
|
287
|
+
if (!p.lastSeen) return false;
|
|
288
|
+
const daysSince = daysBetween(new Date(p.lastSeen), now);
|
|
289
|
+
return daysSince <= 30;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Average pattern age
|
|
293
|
+
const avgAge = patterns.length > 0
|
|
294
|
+
? Math.round(patterns.reduce((sum, p) => {
|
|
295
|
+
if (!p.createdAt) return sum;
|
|
296
|
+
return sum + daysBetween(new Date(p.createdAt), now);
|
|
297
|
+
}, 0) / patterns.length)
|
|
298
|
+
: 0;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
healthy: decayStatus.healthy.length,
|
|
302
|
+
warning: decayStatus.warning.length,
|
|
303
|
+
critical: decayStatus.critical.length,
|
|
304
|
+
recentlyRenewed: recentlyRenewed.length,
|
|
305
|
+
avgAge,
|
|
306
|
+
needsAttention: decayStatus.critical.map(p => ({
|
|
307
|
+
id: p.id,
|
|
308
|
+
questionId: p.questionId,
|
|
309
|
+
confidence: p.confidence,
|
|
310
|
+
lastSeen: p.lastSeen
|
|
311
|
+
}))
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Calculate session timeline
|
|
317
|
+
* @param {Object} skipHistory - Skip history
|
|
318
|
+
* @param {Object} answerHistory - Answer history
|
|
319
|
+
* @returns {Object} Session timeline data
|
|
320
|
+
*/
|
|
321
|
+
function calculateSessionTimeline(skipHistory, answerHistory) {
|
|
322
|
+
const sessions = skipHistory.sessions;
|
|
323
|
+
const timeline = [];
|
|
324
|
+
|
|
325
|
+
for (const session of sessions) {
|
|
326
|
+
const skipCount = (session.skips || []).length;
|
|
327
|
+
const matchingAnswerSession = answerHistory.sessions.find(
|
|
328
|
+
s => s.sessionId === session.sessionId
|
|
329
|
+
);
|
|
330
|
+
const answerCount = matchingAnswerSession ? (matchingAnswerSession.answers || []).length : 0;
|
|
331
|
+
|
|
332
|
+
timeline.push({
|
|
333
|
+
sessionId: session.sessionId,
|
|
334
|
+
timestamp: session.timestamp,
|
|
335
|
+
projectType: session.projectType,
|
|
336
|
+
frameworks: session.frameworks,
|
|
337
|
+
skipCount,
|
|
338
|
+
answerCount,
|
|
339
|
+
totalQuestions: skipCount + answerCount
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Sort by timestamp descending (most recent first)
|
|
344
|
+
timeline.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
345
|
+
|
|
346
|
+
// Calculate frequency
|
|
347
|
+
let sessionsPerWeek = 0;
|
|
348
|
+
let firstSession = null;
|
|
349
|
+
let lastSession = null;
|
|
350
|
+
|
|
351
|
+
if (sessions.length > 0 && sessions[0].timestamp) {
|
|
352
|
+
firstSession = new Date(sessions[0].timestamp).toISOString().split('T')[0];
|
|
353
|
+
|
|
354
|
+
const lastSessionData = sessions[sessions.length - 1];
|
|
355
|
+
if (lastSessionData.timestamp) {
|
|
356
|
+
lastSession = new Date(lastSessionData.timestamp).toISOString().split('T')[0];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (sessions[0].timestamp && lastSessionData.timestamp) {
|
|
360
|
+
const weeksBetween = Math.ceil(daysBetween(new Date(sessions[0].timestamp), new Date(lastSessionData.timestamp)) / 7);
|
|
361
|
+
sessionsPerWeek = weeksBetween > 0 ? (sessions.length / weeksBetween).toFixed(1) : sessions.length;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
timeline,
|
|
367
|
+
sessionsPerWeek,
|
|
368
|
+
totalSessions: sessions.length,
|
|
369
|
+
firstSession,
|
|
370
|
+
lastSession
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Calculate most impactful patterns
|
|
376
|
+
* @param {Array} patterns - Array of patterns
|
|
377
|
+
* @param {Array} rules - Array of rules
|
|
378
|
+
* @param {Object} skipHistory - Skip history
|
|
379
|
+
* @returns {Object} Impactful patterns data
|
|
380
|
+
*/
|
|
381
|
+
function calculateImpactfulPatterns(patterns, rules, skipHistory) {
|
|
382
|
+
const impactData = [];
|
|
383
|
+
|
|
384
|
+
for (const pattern of patterns) {
|
|
385
|
+
const matchingRule = rules.find(r => r.patternId === pattern.id);
|
|
386
|
+
const appliedCount = matchingRule ? (matchingRule.appliedCount || 0) : 0;
|
|
387
|
+
const timeSaved = appliedCount * 2.5; // 2.5 min per question
|
|
388
|
+
|
|
389
|
+
impactData.push({
|
|
390
|
+
pattern: {
|
|
391
|
+
id: pattern.id,
|
|
392
|
+
questionId: pattern.questionId,
|
|
393
|
+
questionText: pattern.questionText,
|
|
394
|
+
confidence: pattern.confidence,
|
|
395
|
+
type: pattern.type
|
|
396
|
+
},
|
|
397
|
+
appliedCount,
|
|
398
|
+
timeSaved: Math.round(timeSaved),
|
|
399
|
+
impact: calculateImpact(appliedCount, pattern.confidence)
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
topByTimeSaved: impactData
|
|
405
|
+
.sort((a, b) => b.timeSaved - a.timeSaved)
|
|
406
|
+
.slice(0, 5),
|
|
407
|
+
topByApplications: impactData
|
|
408
|
+
.sort((a, b) => b.appliedCount - a.appliedCount)
|
|
409
|
+
.slice(0, 5),
|
|
410
|
+
perfectConfidence: patterns.filter(p => p.confidence === 100),
|
|
411
|
+
newestHighConfidence: patterns
|
|
412
|
+
.filter(p => p.confidence >= 90)
|
|
413
|
+
.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0))
|
|
414
|
+
.slice(0, 5)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Calculate question statistics
|
|
420
|
+
* @param {Object} skipHistory - Skip history
|
|
421
|
+
* @param {Object} answerHistory - Answer history
|
|
422
|
+
* @returns {Object} Question statistics
|
|
423
|
+
*/
|
|
424
|
+
function calculateQuestionStats(skipHistory, answerHistory) {
|
|
425
|
+
const questionData = {};
|
|
426
|
+
|
|
427
|
+
// Aggregate skip data
|
|
428
|
+
for (const session of skipHistory.sessions) {
|
|
429
|
+
for (const skip of session.skips || []) {
|
|
430
|
+
if (!questionData[skip.questionId]) {
|
|
431
|
+
questionData[skip.questionId] = {
|
|
432
|
+
questionId: skip.questionId,
|
|
433
|
+
text: skip.text,
|
|
434
|
+
category: skip.category,
|
|
435
|
+
skips: 0,
|
|
436
|
+
answers: 0,
|
|
437
|
+
totalTimeSpent: 0
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
questionData[skip.questionId].skips++;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Aggregate answer data
|
|
445
|
+
for (const session of answerHistory.sessions) {
|
|
446
|
+
for (const answer of session.answers || []) {
|
|
447
|
+
if (!questionData[answer.questionId]) {
|
|
448
|
+
questionData[answer.questionId] = {
|
|
449
|
+
questionId: answer.questionId,
|
|
450
|
+
text: answer.text,
|
|
451
|
+
category: answer.category,
|
|
452
|
+
skips: 0,
|
|
453
|
+
answers: 0,
|
|
454
|
+
totalTimeSpent: 0
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
questionData[answer.questionId].answers++;
|
|
458
|
+
questionData[answer.questionId].totalTimeSpent += answer.timeSpent || 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Calculate rates and averages
|
|
463
|
+
const stats = Object.values(questionData).map(q => {
|
|
464
|
+
const total = q.skips + q.answers;
|
|
465
|
+
q.skipRate = total > 0 ? Math.round((q.skips / total) * 100) : 0;
|
|
466
|
+
q.avgTimeSpent = q.answers > 0 ? Math.round(q.totalTimeSpent / q.answers) : 0;
|
|
467
|
+
return q;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
mostAnswered: stats
|
|
472
|
+
.sort((a, b) => b.answers - a.answers)
|
|
473
|
+
.slice(0, 10),
|
|
474
|
+
mostSkipped: stats
|
|
475
|
+
.sort((a, b) => b.skips - a.skips)
|
|
476
|
+
.slice(0, 10),
|
|
477
|
+
lowestSkipRate: stats
|
|
478
|
+
.filter(q => q.skips + q.answers > 0)
|
|
479
|
+
.sort((a, b) => a.skipRate - b.skipRate)
|
|
480
|
+
.slice(0, 10)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// =============================================================================
|
|
485
|
+
// Helper Functions
|
|
486
|
+
// =============================================================================
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Calculate days between two dates
|
|
490
|
+
* @param {Date} date1 - Start date
|
|
491
|
+
* @param {Date} date2 - End date
|
|
492
|
+
* @returns {number} Days between dates
|
|
493
|
+
*/
|
|
494
|
+
function daysBetween(date1, date2) {
|
|
495
|
+
return Math.floor((date2 - date1) / (1000 * 60 * 60 * 24));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Get last 12 weeks
|
|
500
|
+
* @returns {Array} Array of week objects
|
|
501
|
+
*/
|
|
502
|
+
function getLast12Weeks() {
|
|
503
|
+
const weeks = [];
|
|
504
|
+
const now = new Date();
|
|
505
|
+
|
|
506
|
+
for (let i = 11; i >= 0; i--) {
|
|
507
|
+
const weekStart = new Date(now);
|
|
508
|
+
weekStart.setDate(now.getDate() - (i * 7));
|
|
509
|
+
const weekEnd = new Date(weekStart);
|
|
510
|
+
weekEnd.setDate(weekStart.getDate() + 6);
|
|
511
|
+
|
|
512
|
+
weeks.push({
|
|
513
|
+
label: `Week ${getWeekNumber(weekStart)}`,
|
|
514
|
+
weekNumber: getWeekNumber(weekStart),
|
|
515
|
+
start: weekStart,
|
|
516
|
+
end: weekEnd
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return weeks;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get week number for a date
|
|
525
|
+
* @param {Date} date - Date
|
|
526
|
+
* @returns {number} Week number
|
|
527
|
+
*/
|
|
528
|
+
function getWeekNumber(date) {
|
|
529
|
+
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
|
|
530
|
+
const pastDaysOfYear = (date - firstDayOfYear) / 86400000;
|
|
531
|
+
return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Filter sessions by week
|
|
536
|
+
* @param {Array} sessions - Array of sessions
|
|
537
|
+
* @param {Object} week - Week object
|
|
538
|
+
* @returns {Array} Filtered sessions
|
|
539
|
+
*/
|
|
540
|
+
function filterSessionsByWeek(sessions, week) {
|
|
541
|
+
return sessions.filter(session => {
|
|
542
|
+
const sessionDate = new Date(session.timestamp);
|
|
543
|
+
return sessionDate >= week.start && sessionDate <= week.end;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Count skips by reason in sessions
|
|
549
|
+
* @param {Array} sessions - Array of sessions
|
|
550
|
+
* @param {string} reason - Reason ('manual' or 'filtered')
|
|
551
|
+
* @returns {number} Skip count
|
|
552
|
+
*/
|
|
553
|
+
function countSkipsByReason(sessions, reason) {
|
|
554
|
+
return sessions.reduce((count, session) => {
|
|
555
|
+
return count + (session.skips || []).filter(s => s.reason === reason).length;
|
|
556
|
+
}, 0);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Count answers in sessions
|
|
561
|
+
* @param {Array} sessions - Array of sessions
|
|
562
|
+
* @returns {number} Answer count
|
|
563
|
+
*/
|
|
564
|
+
function countAnswersInSessions(sessions) {
|
|
565
|
+
return sessions.reduce((count, session) => {
|
|
566
|
+
return count + (session.answers || []).length;
|
|
567
|
+
}, 0);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get skip level from skip rate
|
|
572
|
+
* @param {number} skipRate - Skip rate percentage
|
|
573
|
+
* @returns {string} Level ('high', 'medium', 'low')
|
|
574
|
+
*/
|
|
575
|
+
function getSkipLevel(skipRate) {
|
|
576
|
+
if (skipRate >= 70) return 'high';
|
|
577
|
+
if (skipRate >= 40) return 'medium';
|
|
578
|
+
return 'low';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Check if pattern is inactive
|
|
583
|
+
* @param {Object} pattern - Pattern object
|
|
584
|
+
* @param {number} months - Months threshold
|
|
585
|
+
* @returns {boolean} True if inactive
|
|
586
|
+
*/
|
|
587
|
+
function isInactive(pattern, months) {
|
|
588
|
+
if (!pattern.lastSeen) return true;
|
|
589
|
+
const now = new Date();
|
|
590
|
+
const lastSeen = new Date(pattern.lastSeen);
|
|
591
|
+
const monthsInactive = (now - lastSeen) / (1000 * 60 * 60 * 24 * 30);
|
|
592
|
+
return monthsInactive >= months;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Calculate impact score
|
|
597
|
+
* @param {number} applications - Number of applications
|
|
598
|
+
* @param {number} confidence - Confidence score
|
|
599
|
+
* @returns {number} Impact score
|
|
600
|
+
*/
|
|
601
|
+
function calculateImpact(applications, confidence) {
|
|
602
|
+
return Math.round(applications * confidence * 0.01);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Count total skips
|
|
607
|
+
* @param {Object} skipHistory - Skip history
|
|
608
|
+
* @returns {number} Total skips
|
|
609
|
+
*/
|
|
610
|
+
function countTotalSkips(skipHistory) {
|
|
611
|
+
return skipHistory.sessions.reduce((count, session) => {
|
|
612
|
+
return count + (session.skips || []).length;
|
|
613
|
+
}, 0);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Count manual skips
|
|
618
|
+
* @param {Object} skipHistory - Skip history
|
|
619
|
+
* @returns {number} Manual skip count
|
|
620
|
+
*/
|
|
621
|
+
function countManualSkips(skipHistory) {
|
|
622
|
+
return skipHistory.sessions.reduce((count, session) => {
|
|
623
|
+
return count + (session.skips || []).filter(s => s.reason === 'manual').length;
|
|
624
|
+
}, 0);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Count filtered skips
|
|
629
|
+
* @param {Object} skipHistory - Skip history
|
|
630
|
+
* @returns {number} Filtered skip count
|
|
631
|
+
*/
|
|
632
|
+
function countFilteredSkips(skipHistory) {
|
|
633
|
+
return skipHistory.sessions.reduce((count, session) => {
|
|
634
|
+
return count + (session.skips || []).filter(s => s.reason === 'filtered').length;
|
|
635
|
+
}, 0);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Count total answers
|
|
640
|
+
* @param {Object} answerHistory - Answer history
|
|
641
|
+
* @returns {number} Total answers
|
|
642
|
+
*/
|
|
643
|
+
function countTotalAnswers(answerHistory) {
|
|
644
|
+
return answerHistory.sessions.reduce((count, session) => {
|
|
645
|
+
return count + (session.answers || []).length;
|
|
646
|
+
}, 0);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Count filtered questions
|
|
651
|
+
* @param {Object} skipHistory - Skip history
|
|
652
|
+
* @returns {number} Filtered question count
|
|
653
|
+
*/
|
|
654
|
+
function countFilteredQuestions(skipHistory) {
|
|
655
|
+
return countFilteredSkips(skipHistory);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Calculate days spanned by sessions
|
|
660
|
+
* @param {Array} sessions - Array of sessions
|
|
661
|
+
* @returns {number} Days spanned
|
|
662
|
+
*/
|
|
663
|
+
function calculateDaysSpanned(sessions) {
|
|
664
|
+
if (sessions.length === 0) return 0;
|
|
665
|
+
const first = new Date(sessions[0].timestamp);
|
|
666
|
+
const last = new Date(sessions[sessions.length - 1].timestamp);
|
|
667
|
+
return daysBetween(first, last);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
module.exports = {
|
|
671
|
+
generateAnalytics,
|
|
672
|
+
calculateOverviewStats,
|
|
673
|
+
calculateSkipTrends,
|
|
674
|
+
calculateCategoryPreferences,
|
|
675
|
+
calculatePatternDistribution,
|
|
676
|
+
calculateEffectiveness,
|
|
677
|
+
calculateDecayStatus,
|
|
678
|
+
calculateSessionTimeline,
|
|
679
|
+
calculateImpactfulPatterns,
|
|
680
|
+
calculateQuestionStats
|
|
681
|
+
};
|