pikakit 1.0.8 → 1.0.10

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,523 @@
1
+ /**
2
+ * AutoLearn v6.0 - Metrics Collector
3
+ *
4
+ * Collects, stores, and retrieves metrics for Dashboard.
5
+ * All metrics are measurable and tracked over time.
6
+ *
7
+ * @version 6.0.0
8
+ * @author PikaKit
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { METRICS_SCHEMA, getAllMetricIds, validateMetric } from './metrics-schema.js';
14
+
15
+ // ============================================================================
16
+ // CONFIGURATION
17
+ // ============================================================================
18
+
19
+ const METRICS_DIR = path.join(process.cwd(), '.agent', 'knowledge');
20
+ const METRICS_FILE = path.join(METRICS_DIR, 'autolearn-metrics.json');
21
+ const HISTORY_FILE = path.join(METRICS_DIR, 'metrics-history.json');
22
+
23
+ // Maximum history entries per metric (30 days of hourly data)
24
+ const MAX_HISTORY_ENTRIES = 720;
25
+
26
+ // ============================================================================
27
+ // DATA STRUCTURES
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Initial metrics state
32
+ */
33
+ function createInitialMetrics() {
34
+ const metrics = {
35
+ version: '6.0.0',
36
+ createdAt: new Date().toISOString(),
37
+ updatedAt: new Date().toISOString(),
38
+
39
+ // Raw counters (used to calculate rates)
40
+ counters: {
41
+ total_tasks: 0,
42
+ successful_tasks: 0,
43
+ failed_tasks: 0,
44
+ total_errors: 0,
45
+ repeated_errors: 0,
46
+ no_retry_tasks: 0,
47
+ manual_fixes: 0,
48
+ true_positives: 0,
49
+ false_positives: 0,
50
+ false_negatives: 0,
51
+ total_alerts: 0,
52
+ tasks_with_skill: 0,
53
+ tasks_helped_by_skill: 0,
54
+ tasks_where_skill_applied: 0,
55
+ auto_skills_created: 0,
56
+ skills_pruned: 0,
57
+ total_ab_tests: 0,
58
+ ab_tests_with_winner: 0,
59
+ new_patterns_this_week: 0,
60
+ baseline_errors: 0,
61
+ current_errors: 0,
62
+ total_time_saved_seconds: 0,
63
+ total_skill_creation_time_seconds: 0
64
+ },
65
+
66
+ // Aggregated values
67
+ aggregates: {
68
+ total_task_duration_seconds: 0,
69
+ pattern_confidence_sum: 0,
70
+ pattern_count: 0
71
+ },
72
+
73
+ // Calculated KPIs (updated after each event)
74
+ kpis: {},
75
+
76
+ // Weekly snapshots for trend analysis
77
+ weekly: {
78
+ current_week_start: getWeekStart().toISOString(),
79
+ last_week_success_rate: null,
80
+ this_week_success_rate: null
81
+ }
82
+ };
83
+
84
+ // Initialize all KPIs to 0
85
+ getAllMetricIds().forEach(id => {
86
+ metrics.kpis[id] = 0;
87
+ });
88
+
89
+ return metrics;
90
+ }
91
+
92
+ /**
93
+ * Get start of current week (Monday)
94
+ */
95
+ function getWeekStart() {
96
+ const now = new Date();
97
+ const day = now.getDay();
98
+ const diff = now.getDate() - day + (day === 0 ? -6 : 1);
99
+ const weekStart = new Date(now.setDate(diff));
100
+ weekStart.setHours(0, 0, 0, 0);
101
+ return weekStart;
102
+ }
103
+
104
+ // ============================================================================
105
+ // STORAGE OPERATIONS
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Load metrics from disk
110
+ * @returns {Object} - Metrics object
111
+ */
112
+ export function loadMetrics() {
113
+ try {
114
+ if (!fs.existsSync(METRICS_FILE)) {
115
+ const initial = createInitialMetrics();
116
+ saveMetrics(initial);
117
+ return initial;
118
+ }
119
+
120
+ const content = fs.readFileSync(METRICS_FILE, 'utf8');
121
+ return JSON.parse(content);
122
+ } catch (error) {
123
+ console.error('Error loading metrics:', error.message);
124
+ return createInitialMetrics();
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Save metrics to disk
130
+ * @param {Object} metrics - Metrics object
131
+ */
132
+ export function saveMetrics(metrics) {
133
+ try {
134
+ if (!fs.existsSync(METRICS_DIR)) {
135
+ fs.mkdirSync(METRICS_DIR, { recursive: true });
136
+ }
137
+
138
+ metrics.updatedAt = new Date().toISOString();
139
+ fs.writeFileSync(METRICS_FILE, JSON.stringify(metrics, null, 2), 'utf8');
140
+ } catch (error) {
141
+ console.error('Error saving metrics:', error.message);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Append to metrics history
147
+ * @param {Object} snapshot - Current KPI snapshot
148
+ */
149
+ function appendHistory(snapshot) {
150
+ try {
151
+ let history = [];
152
+
153
+ if (fs.existsSync(HISTORY_FILE)) {
154
+ history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
155
+ }
156
+
157
+ history.push({
158
+ timestamp: new Date().toISOString(),
159
+ ...snapshot
160
+ });
161
+
162
+ // Keep only recent entries
163
+ if (history.length > MAX_HISTORY_ENTRIES) {
164
+ history = history.slice(-MAX_HISTORY_ENTRIES);
165
+ }
166
+
167
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), 'utf8');
168
+ } catch (error) {
169
+ console.error('Error appending history:', error.message);
170
+ }
171
+ }
172
+
173
+ // ============================================================================
174
+ // KPI CALCULATION
175
+ // ============================================================================
176
+
177
+ /**
178
+ * Recalculate all KPIs from counters
179
+ * @param {Object} metrics - Metrics object
180
+ * @returns {Object} - Updated metrics with KPIs
181
+ */
182
+ export function recalculateKPIs(metrics) {
183
+ const c = metrics.counters;
184
+ const a = metrics.aggregates;
185
+ const kpis = {};
186
+
187
+ // Core Metrics (1-5)
188
+ kpis.task_success_rate = c.total_tasks > 0
189
+ ? (c.successful_tasks / c.total_tasks * 100).toFixed(2)
190
+ : 0;
191
+
192
+ kpis.error_repeat_rate = c.total_errors > 0
193
+ ? (c.repeated_errors / c.total_errors * 100).toFixed(2)
194
+ : 0;
195
+
196
+ kpis.first_time_success = c.total_tasks > 0
197
+ ? (c.no_retry_tasks / c.total_tasks * 100).toFixed(2)
198
+ : 0;
199
+
200
+ kpis.time_to_completion = c.total_tasks > 0
201
+ ? (a.total_task_duration_seconds / c.total_tasks).toFixed(2)
202
+ : 0;
203
+
204
+ kpis.human_intervention_rate = c.total_tasks > 0
205
+ ? (c.manual_fixes / c.total_tasks * 100).toFixed(2)
206
+ : 0;
207
+
208
+ // Learning Metrics (6-10)
209
+ const totalDetections = c.true_positives + c.false_positives;
210
+ kpis.pattern_precision = totalDetections > 0
211
+ ? (c.true_positives / totalDetections * 100).toFixed(2)
212
+ : 0;
213
+
214
+ const totalActual = c.true_positives + c.false_negatives;
215
+ kpis.pattern_recall = totalActual > 0
216
+ ? (c.true_positives / totalActual * 100).toFixed(2)
217
+ : 0;
218
+
219
+ kpis.skill_effectiveness = c.tasks_where_skill_applied > 0
220
+ ? (c.tasks_helped_by_skill / c.tasks_where_skill_applied * 100).toFixed(2)
221
+ : 0;
222
+
223
+ kpis.skill_coverage = c.total_tasks > 0
224
+ ? (c.tasks_with_skill / c.total_tasks * 100).toFixed(2)
225
+ : 0;
226
+
227
+ kpis.false_positive_rate = c.total_alerts > 0
228
+ ? (c.false_positives / c.total_alerts * 100).toFixed(2)
229
+ : 0;
230
+
231
+ // Evolution Metrics (11-15)
232
+ kpis.skills_auto_generated = c.auto_skills_created;
233
+ kpis.skills_pruned = c.skills_pruned;
234
+
235
+ kpis.pattern_confidence_avg = a.pattern_count > 0
236
+ ? (a.pattern_confidence_sum / a.pattern_count).toFixed(3)
237
+ : 0;
238
+
239
+ kpis.ab_test_win_rate = c.total_ab_tests > 0
240
+ ? (c.ab_tests_with_winner / c.total_ab_tests * 100).toFixed(2)
241
+ : 0;
242
+
243
+ kpis.learning_velocity = c.new_patterns_this_week;
244
+
245
+ // Improvement Metrics (16-18)
246
+ const lastWeek = metrics.weekly.last_week_success_rate;
247
+ const thisWeek = parseFloat(kpis.task_success_rate);
248
+ kpis.week_over_week_improvement = lastWeek && lastWeek > 0
249
+ ? ((thisWeek - lastWeek) / lastWeek * 100).toFixed(2)
250
+ : 0;
251
+
252
+ kpis.error_reduction_rate = c.baseline_errors > 0
253
+ ? ((1 - c.current_errors / c.baseline_errors) * 100).toFixed(2)
254
+ : 0;
255
+
256
+ kpis.skill_roi = c.total_skill_creation_time_seconds > 0
257
+ ? (c.total_time_saved_seconds / c.total_skill_creation_time_seconds).toFixed(2)
258
+ : 0;
259
+
260
+ metrics.kpis = kpis;
261
+ return metrics;
262
+ }
263
+
264
+ // ============================================================================
265
+ // EVENT RECORDING
266
+ // ============================================================================
267
+
268
+ /**
269
+ * Record a task event
270
+ * @param {Object} event - Task event details
271
+ */
272
+ export function recordTaskEvent(event) {
273
+ const metrics = loadMetrics();
274
+ const c = metrics.counters;
275
+ const a = metrics.aggregates;
276
+
277
+ c.total_tasks++;
278
+
279
+ if (event.success) {
280
+ c.successful_tasks++;
281
+ } else {
282
+ c.failed_tasks++;
283
+ c.total_errors++;
284
+
285
+ // Check if this error is a repeat
286
+ if (event.errorId && event.isRepeat) {
287
+ c.repeated_errors++;
288
+ }
289
+ }
290
+
291
+ if (event.firstAttempt) {
292
+ c.no_retry_tasks++;
293
+ }
294
+
295
+ if (event.manualFix) {
296
+ c.manual_fixes++;
297
+ }
298
+
299
+ if (event.duration) {
300
+ a.total_task_duration_seconds += event.duration;
301
+ }
302
+
303
+ if (event.skillApplied) {
304
+ c.tasks_with_skill++;
305
+ c.tasks_where_skill_applied++;
306
+
307
+ if (event.skillHelped) {
308
+ c.tasks_helped_by_skill++;
309
+ }
310
+ }
311
+
312
+ recalculateKPIs(metrics);
313
+ saveMetrics(metrics);
314
+
315
+ return metrics;
316
+ }
317
+
318
+ /**
319
+ * Record a pattern event
320
+ * @param {Object} event - Pattern event details
321
+ */
322
+ export function recordPatternEvent(event) {
323
+ const metrics = loadMetrics();
324
+ const c = metrics.counters;
325
+ const a = metrics.aggregates;
326
+
327
+ c.total_alerts++;
328
+
329
+ if (event.type === 'true_positive') {
330
+ c.true_positives++;
331
+ } else if (event.type === 'false_positive') {
332
+ c.false_positives++;
333
+ } else if (event.type === 'false_negative') {
334
+ c.false_negatives++;
335
+ }
336
+
337
+ if (event.confidence !== undefined) {
338
+ a.pattern_confidence_sum += event.confidence;
339
+ a.pattern_count++;
340
+ }
341
+
342
+ if (event.newPattern) {
343
+ c.new_patterns_this_week++;
344
+ }
345
+
346
+ recalculateKPIs(metrics);
347
+ saveMetrics(metrics);
348
+
349
+ return metrics;
350
+ }
351
+
352
+ /**
353
+ * Record a skill event
354
+ * @param {Object} event - Skill event details
355
+ */
356
+ export function recordSkillEvent(event) {
357
+ const metrics = loadMetrics();
358
+ const c = metrics.counters;
359
+
360
+ if (event.type === 'created') {
361
+ c.auto_skills_created++;
362
+ if (event.creationTime) {
363
+ c.total_skill_creation_time_seconds += event.creationTime;
364
+ }
365
+ } else if (event.type === 'pruned') {
366
+ c.skills_pruned++;
367
+ }
368
+
369
+ if (event.timeSaved) {
370
+ c.total_time_saved_seconds += event.timeSaved;
371
+ }
372
+
373
+ recalculateKPIs(metrics);
374
+ saveMetrics(metrics);
375
+
376
+ return metrics;
377
+ }
378
+
379
+ /**
380
+ * Record an A/B test result
381
+ * @param {Object} event - A/B test event details
382
+ */
383
+ export function recordABTestEvent(event) {
384
+ const metrics = loadMetrics();
385
+ const c = metrics.counters;
386
+
387
+ c.total_ab_tests++;
388
+
389
+ if (event.hasWinner) {
390
+ c.ab_tests_with_winner++;
391
+ }
392
+
393
+ recalculateKPIs(metrics);
394
+ saveMetrics(metrics);
395
+
396
+ return metrics;
397
+ }
398
+
399
+ /**
400
+ * Set baseline for error reduction tracking
401
+ */
402
+ export function setErrorBaseline() {
403
+ const metrics = loadMetrics();
404
+ metrics.counters.baseline_errors = metrics.counters.total_errors;
405
+ metrics.counters.current_errors = 0;
406
+ saveMetrics(metrics);
407
+ }
408
+
409
+ /**
410
+ * Rotate weekly metrics (call at week start)
411
+ */
412
+ export function rotateWeeklyMetrics() {
413
+ const metrics = loadMetrics();
414
+ const weekStart = getWeekStart();
415
+
416
+ // Save last week's success rate
417
+ metrics.weekly.last_week_success_rate = parseFloat(metrics.kpis.task_success_rate) || null;
418
+
419
+ // Reset weekly counters
420
+ metrics.counters.new_patterns_this_week = 0;
421
+ metrics.weekly.current_week_start = weekStart.toISOString();
422
+
423
+ recalculateKPIs(metrics);
424
+ saveMetrics(metrics);
425
+
426
+ // Take snapshot for history
427
+ appendHistory(metrics.kpis);
428
+
429
+ return metrics;
430
+ }
431
+
432
+ // ============================================================================
433
+ // DASHBOARD API
434
+ // ============================================================================
435
+
436
+ /**
437
+ * Get all current KPIs for Dashboard
438
+ * @returns {Object} - KPIs with validation status
439
+ */
440
+ export function getDashboardData() {
441
+ const metrics = loadMetrics();
442
+ const dashboard = {
443
+ updatedAt: metrics.updatedAt,
444
+ kpis: {}
445
+ };
446
+
447
+ Object.entries(metrics.kpis).forEach(([id, value]) => {
448
+ const validation = validateMetric(id, parseFloat(value));
449
+ const schema = Object.values(METRICS_SCHEMA).find(m => m.id === id);
450
+
451
+ dashboard.kpis[id] = {
452
+ value: parseFloat(value),
453
+ status: validation.status,
454
+ message: validation.message,
455
+ widget: schema?.dashboard?.widget || 'number',
456
+ color: schema?.dashboard?.color || 'gray',
457
+ name: schema?.name || id,
458
+ unit: schema?.unit || ''
459
+ };
460
+ });
461
+
462
+ return dashboard;
463
+ }
464
+
465
+ /**
466
+ * Get metrics history for trend charts
467
+ * @param {string} metricId - Metric ID to get history for
468
+ * @param {number} limit - Number of entries to return
469
+ * @returns {Array} - Array of { timestamp, value } objects
470
+ */
471
+ export function getMetricHistory(metricId, limit = 30) {
472
+ try {
473
+ if (!fs.existsSync(HISTORY_FILE)) {
474
+ return [];
475
+ }
476
+
477
+ const history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
478
+
479
+ return history
480
+ .slice(-limit)
481
+ .map(entry => ({
482
+ timestamp: entry.timestamp,
483
+ value: entry[metricId] || 0
484
+ }));
485
+ } catch (error) {
486
+ return [];
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Get summary statistics
492
+ * @returns {Object} - Summary object
493
+ */
494
+ export function getSummary() {
495
+ const metrics = loadMetrics();
496
+
497
+ return {
498
+ totalTasks: metrics.counters.total_tasks,
499
+ successRate: metrics.kpis.task_success_rate,
500
+ skillsGenerated: metrics.counters.auto_skills_created,
501
+ patternsLearned: metrics.aggregates.pattern_count,
502
+ lastUpdated: metrics.updatedAt
503
+ };
504
+ }
505
+
506
+ // ============================================================================
507
+ // EXPORTS
508
+ // ============================================================================
509
+
510
+ export default {
511
+ loadMetrics,
512
+ saveMetrics,
513
+ recalculateKPIs,
514
+ recordTaskEvent,
515
+ recordPatternEvent,
516
+ recordSkillEvent,
517
+ recordABTestEvent,
518
+ setErrorBaseline,
519
+ rotateWeeklyMetrics,
520
+ getDashboardData,
521
+ getMetricHistory,
522
+ getSummary
523
+ };