pikakit 2.0.0 → 3.0.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.
Files changed (92) hide show
  1. package/README.md +8 -8
  2. package/lib/agent-cli/bin/agent.js +187 -0
  3. package/lib/agent-cli/lib/audit.js +154 -0
  4. package/lib/agent-cli/lib/audit.test.js +100 -0
  5. package/lib/agent-cli/lib/auto-learn.js +319 -0
  6. package/lib/agent-cli/lib/backup.js +138 -0
  7. package/lib/agent-cli/lib/backup.test.js +78 -0
  8. package/lib/agent-cli/lib/cognitive-lesson.js +476 -0
  9. package/lib/agent-cli/lib/completion.js +149 -0
  10. package/lib/agent-cli/lib/config.js +35 -0
  11. package/lib/agent-cli/lib/eslint-fix.js +238 -0
  12. package/lib/agent-cli/lib/evolution-signal.js +215 -0
  13. package/lib/agent-cli/lib/export.js +86 -0
  14. package/lib/agent-cli/lib/export.test.js +65 -0
  15. package/lib/agent-cli/lib/fix.js +337 -0
  16. package/lib/agent-cli/lib/fix.test.js +80 -0
  17. package/lib/agent-cli/lib/gemini-export.js +83 -0
  18. package/lib/agent-cli/lib/generate-registry.js +42 -0
  19. package/lib/agent-cli/lib/hooks/install-hooks.js +152 -0
  20. package/lib/agent-cli/lib/hooks/lint-learn.js +172 -0
  21. package/lib/agent-cli/lib/icons.js +93 -0
  22. package/lib/agent-cli/lib/ignore.js +116 -0
  23. package/lib/agent-cli/lib/ignore.test.js +58 -0
  24. package/lib/agent-cli/lib/init.js +124 -0
  25. package/lib/agent-cli/lib/knowledge-index.js +326 -0
  26. package/lib/agent-cli/lib/knowledge-metrics.js +335 -0
  27. package/lib/agent-cli/lib/knowledge-retention.js +398 -0
  28. package/lib/agent-cli/lib/knowledge-validator.js +312 -0
  29. package/lib/agent-cli/lib/learn.js +255 -0
  30. package/lib/agent-cli/lib/learn.test.js +70 -0
  31. package/lib/agent-cli/lib/proposals.js +199 -0
  32. package/lib/agent-cli/lib/proposals.test.js +56 -0
  33. package/lib/agent-cli/lib/recall.js +826 -0
  34. package/lib/agent-cli/lib/recall.test.js +107 -0
  35. package/lib/agent-cli/lib/selfevolution-bridge.js +167 -0
  36. package/lib/agent-cli/lib/settings.js +203 -0
  37. package/lib/agent-cli/lib/skill-learn.js +296 -0
  38. package/lib/agent-cli/lib/stats.js +132 -0
  39. package/lib/agent-cli/lib/stats.test.js +94 -0
  40. package/lib/agent-cli/lib/types.js +33 -0
  41. package/lib/agent-cli/lib/ui/audit-ui.js +146 -0
  42. package/lib/agent-cli/lib/ui/backup-ui.js +107 -0
  43. package/lib/agent-cli/lib/ui/clack-helpers.js +317 -0
  44. package/lib/agent-cli/lib/ui/common.js +83 -0
  45. package/lib/agent-cli/lib/ui/completion-ui.js +126 -0
  46. package/lib/agent-cli/lib/ui/custom-select.js +69 -0
  47. package/lib/agent-cli/lib/ui/dashboard-ui.js +222 -0
  48. package/lib/agent-cli/lib/ui/evolution-signals-ui.js +107 -0
  49. package/lib/agent-cli/lib/ui/export-ui.js +94 -0
  50. package/lib/agent-cli/lib/ui/fix-all-ui.js +191 -0
  51. package/lib/agent-cli/lib/ui/help-ui.js +49 -0
  52. package/lib/agent-cli/lib/ui/index.js +199 -0
  53. package/lib/agent-cli/lib/ui/init-ui.js +56 -0
  54. package/lib/agent-cli/lib/ui/knowledge-ui.js +55 -0
  55. package/lib/agent-cli/lib/ui/learn-ui.js +706 -0
  56. package/lib/agent-cli/lib/ui/lessons-ui.js +148 -0
  57. package/lib/agent-cli/lib/ui/pretty.js +145 -0
  58. package/lib/agent-cli/lib/ui/proposals-ui.js +99 -0
  59. package/lib/agent-cli/lib/ui/recall-ui.js +342 -0
  60. package/lib/agent-cli/lib/ui/routing-demo.js +79 -0
  61. package/lib/agent-cli/lib/ui/routing-ui.js +325 -0
  62. package/lib/agent-cli/lib/ui/settings-ui.js +381 -0
  63. package/lib/agent-cli/lib/ui/stats-ui.js +123 -0
  64. package/lib/agent-cli/lib/ui/watch-ui.js +236 -0
  65. package/lib/agent-cli/lib/watcher.js +181 -0
  66. package/lib/agent-cli/lib/watcher.test.js +85 -0
  67. package/lib/agent-cli/src/MIGRATION.md +418 -0
  68. package/lib/agent-cli/src/README.md +367 -0
  69. package/lib/agent-cli/src/core/evolution/evolution-signal.js +42 -0
  70. package/lib/agent-cli/src/core/evolution/index.js +17 -0
  71. package/lib/agent-cli/src/core/evolution/review-gate.js +40 -0
  72. package/lib/agent-cli/src/core/evolution/signal-detector.js +137 -0
  73. package/lib/agent-cli/src/core/evolution/signal-queue.js +79 -0
  74. package/lib/agent-cli/src/core/evolution/threshold-checker.js +79 -0
  75. package/lib/agent-cli/src/core/index.js +15 -0
  76. package/lib/agent-cli/src/core/learning/cognitive-enhancer.js +282 -0
  77. package/lib/agent-cli/src/core/learning/index.js +12 -0
  78. package/lib/agent-cli/src/core/learning/lesson-synthesizer.js +83 -0
  79. package/lib/agent-cli/src/core/scanning/index.js +14 -0
  80. package/lib/agent-cli/src/data/index.js +13 -0
  81. package/lib/agent-cli/src/data/repositories/index.js +8 -0
  82. package/lib/agent-cli/src/data/repositories/lesson-repository.js +130 -0
  83. package/lib/agent-cli/src/data/repositories/signal-repository.js +119 -0
  84. package/lib/agent-cli/src/data/storage/index.js +8 -0
  85. package/lib/agent-cli/src/data/storage/json-storage.js +64 -0
  86. package/lib/agent-cli/src/data/storage/yaml-storage.js +66 -0
  87. package/lib/agent-cli/src/infrastructure/index.js +13 -0
  88. package/lib/agent-cli/src/presentation/formatters/skill-formatter.js +232 -0
  89. package/lib/agent-cli/src/services/export-service.js +162 -0
  90. package/lib/agent-cli/src/services/index.js +13 -0
  91. package/lib/agent-cli/src/services/learning-service.js +99 -0
  92. package/package.json +5 -3
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knowledge Index Generator
4
+ *
5
+ * Builds inverted index for O(1) pattern lookup in knowledge base.
6
+ * Regenerates on-demand when knowledge files change.
7
+ *
8
+ * Usage:
9
+ * agent index --rebuild # Force rebuild index
10
+ * agent index --status # Check index freshness
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import yaml from 'js-yaml';
16
+ import { KNOWLEDGE_DIR } from './config.js';
17
+
18
+ const INDEX_PATH = path.join(KNOWLEDGE_DIR, 'index.json');
19
+ const INDEX_VERSION = 1;
20
+
21
+ /**
22
+ * Load all lessons from knowledge base
23
+ * @returns {{ mistakes: Array, improvements: Array }}
24
+ */
25
+ function loadAllLessons() {
26
+ const mistakes = [];
27
+ const improvements = [];
28
+
29
+ // Load mistakes.yaml
30
+ const mistakesPath = path.join(KNOWLEDGE_DIR, 'mistakes.yaml');
31
+ if (fs.existsSync(mistakesPath)) {
32
+ const data = yaml.load(fs.readFileSync(mistakesPath, 'utf8'));
33
+ if (data?.mistakes) {
34
+ mistakes.push(...data.mistakes.map(m => ({ ...m, type: 'mistake' })));
35
+ }
36
+ }
37
+
38
+ // Load improvements.yaml
39
+ const improvementsPath = path.join(KNOWLEDGE_DIR, 'improvements.yaml');
40
+ if (fs.existsSync(improvementsPath)) {
41
+ const data = yaml.load(fs.readFileSync(improvementsPath, 'utf8'));
42
+ if (data?.improvements) {
43
+ improvements.push(...data.improvements.map(i => ({ ...i, type: 'improvement' })));
44
+ }
45
+ }
46
+
47
+ return { mistakes, improvements };
48
+ }
49
+
50
+ /**
51
+ * Build inverted index from lessons
52
+ * @param {Array} lessons - All lessons combined
53
+ * @returns {Object} Index structure
54
+ */
55
+ function buildIndex(lessons) {
56
+ const patternIndex = {}; // pattern word -> lesson IDs
57
+ const tagIndex = {}; // tag -> lesson IDs
58
+ const idIndex = {}; // id -> lesson (for direct lookup)
59
+ const severityIndex = { // severity -> lesson IDs
60
+ ERROR: [],
61
+ WARNING: [],
62
+ INFO: []
63
+ };
64
+
65
+ for (const lesson of lessons) {
66
+ const id = lesson.id;
67
+
68
+ // ID index (direct lookup)
69
+ idIndex[id] = {
70
+ pattern: lesson.pattern,
71
+ message: lesson.message,
72
+ severity: lesson.severity || 'WARNING',
73
+ type: lesson.type,
74
+ hitCount: lesson.hitCount || 0,
75
+ confidence: lesson.cognitive?.confidence || 0.3
76
+ };
77
+
78
+ // Pattern index (tokenize pattern for partial matching)
79
+ if (lesson.pattern) {
80
+ const tokens = tokenizePattern(lesson.pattern);
81
+ for (const token of tokens) {
82
+ if (!patternIndex[token]) {
83
+ patternIndex[token] = [];
84
+ }
85
+ if (!patternIndex[token].includes(id)) {
86
+ patternIndex[token].push(id);
87
+ }
88
+ }
89
+ }
90
+
91
+ // Tag index
92
+ if (lesson.tags && Array.isArray(lesson.tags)) {
93
+ for (const tag of lesson.tags) {
94
+ if (!tagIndex[tag]) {
95
+ tagIndex[tag] = [];
96
+ }
97
+ if (!tagIndex[tag].includes(id)) {
98
+ tagIndex[tag].push(id);
99
+ }
100
+ }
101
+ }
102
+
103
+ // Severity index
104
+ const severity = lesson.severity || 'WARNING';
105
+ if (severityIndex[severity]) {
106
+ severityIndex[severity].push(id);
107
+ }
108
+ }
109
+
110
+ return {
111
+ version: INDEX_VERSION,
112
+ generatedAt: new Date().toISOString(),
113
+ stats: {
114
+ totalLessons: lessons.length,
115
+ totalMistakes: lessons.filter(l => l.type === 'mistake').length,
116
+ totalImprovements: lessons.filter(l => l.type === 'improvement').length,
117
+ totalPatterns: Object.keys(patternIndex).length,
118
+ totalTags: Object.keys(tagIndex).length
119
+ },
120
+ patternIndex,
121
+ tagIndex,
122
+ severityIndex,
123
+ idIndex
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Tokenize a regex pattern into searchable words
129
+ * @param {string} pattern - Regex pattern
130
+ * @returns {string[]} Tokens
131
+ */
132
+ function tokenizePattern(pattern) {
133
+ // Extract alphanumeric words, ignoring regex special chars
134
+ const words = pattern
135
+ .replace(/[\[\]\(\)\{\}\.\*\+\?\^\$\\|]/g, ' ')
136
+ .split(/\s+/)
137
+ .map(w => w.toLowerCase().trim())
138
+ .filter(w => w.length >= 2);
139
+
140
+ return [...new Set(words)];
141
+ }
142
+
143
+ /**
144
+ * Get modification time of knowledge files
145
+ * @returns {number} Most recent mtime
146
+ */
147
+ function getKnowledgeMtime() {
148
+ const files = ['mistakes.yaml', 'improvements.yaml', 'lessons-learned.yaml'];
149
+ let maxMtime = 0;
150
+
151
+ for (const file of files) {
152
+ const filepath = path.join(KNOWLEDGE_DIR, file);
153
+ if (fs.existsSync(filepath)) {
154
+ const stat = fs.statSync(filepath);
155
+ if (stat.mtime.getTime() > maxMtime) {
156
+ maxMtime = stat.mtime.getTime();
157
+ }
158
+ }
159
+ }
160
+
161
+ return maxMtime;
162
+ }
163
+
164
+ /**
165
+ * Check if index is stale
166
+ * @returns {{ stale: boolean, reason?: string }}
167
+ */
168
+ export function checkIndexFreshness() {
169
+ if (!fs.existsSync(INDEX_PATH)) {
170
+ return { stale: true, reason: 'Index does not exist' };
171
+ }
172
+
173
+ try {
174
+ const index = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
175
+ const indexTime = new Date(index.generatedAt).getTime();
176
+ const knowledgeTime = getKnowledgeMtime();
177
+
178
+ if (knowledgeTime > indexTime) {
179
+ return { stale: true, reason: 'Knowledge files modified after index' };
180
+ }
181
+
182
+ if (index.version !== INDEX_VERSION) {
183
+ return { stale: true, reason: `Index version mismatch (${index.version} vs ${INDEX_VERSION})` };
184
+ }
185
+
186
+ return { stale: false };
187
+ } catch (e) {
188
+ return { stale: true, reason: `Index corrupted: ${e.message}` };
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Load or rebuild index
194
+ * @param {boolean} forceRebuild - Force rebuild even if fresh
195
+ * @returns {Object} Index
196
+ */
197
+ export function loadIndex(forceRebuild = false) {
198
+ const freshness = checkIndexFreshness();
199
+
200
+ if (!forceRebuild && !freshness.stale) {
201
+ // Load existing index
202
+ return JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
203
+ }
204
+
205
+ // Rebuild index
206
+ return rebuildIndex();
207
+ }
208
+
209
+ /**
210
+ * Force rebuild index
211
+ * @returns {Object} New index
212
+ */
213
+ export function rebuildIndex() {
214
+ const { mistakes, improvements } = loadAllLessons();
215
+ const allLessons = [...mistakes, ...improvements];
216
+
217
+ const index = buildIndex(allLessons);
218
+
219
+ // Save index
220
+ fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
221
+ fs.writeFileSync(INDEX_PATH, JSON.stringify(index, null, 2), 'utf8');
222
+
223
+ return index;
224
+ }
225
+
226
+ /**
227
+ * Search index by pattern
228
+ * @param {string} query - Search query
229
+ * @param {Object} index - Loaded index
230
+ * @returns {string[]} Matching lesson IDs
231
+ */
232
+ export function searchByPattern(query, index) {
233
+ const tokens = tokenizePattern(query);
234
+ const matchingIds = new Set();
235
+
236
+ for (const token of tokens) {
237
+ if (index.patternIndex[token]) {
238
+ for (const id of index.patternIndex[token]) {
239
+ matchingIds.add(id);
240
+ }
241
+ }
242
+ }
243
+
244
+ return [...matchingIds];
245
+ }
246
+
247
+ /**
248
+ * Search index by tag
249
+ * @param {string} tag - Tag to search
250
+ * @param {Object} index - Loaded index
251
+ * @returns {string[]} Matching lesson IDs
252
+ */
253
+ export function searchByTag(tag, index) {
254
+ return index.tagIndex[tag] || [];
255
+ }
256
+
257
+ /**
258
+ * Get lesson by ID
259
+ * @param {string} id - Lesson ID
260
+ * @param {Object} index - Loaded index
261
+ * @returns {Object|null} Lesson info
262
+ */
263
+ export function getById(id, index) {
264
+ return index.idIndex[id] || null;
265
+ }
266
+
267
+ /**
268
+ * CLI entry point
269
+ */
270
+ function main() {
271
+ const args = process.argv.slice(2);
272
+
273
+ if (args.includes('--status')) {
274
+ const freshness = checkIndexFreshness();
275
+ if (freshness.stale) {
276
+ console.log(`⚠️ Index is STALE: ${freshness.reason}`);
277
+ process.exit(1);
278
+ } else {
279
+ const index = loadIndex();
280
+ console.log(`✅ Index is FRESH`);
281
+ console.log(` Generated: ${index.generatedAt}`);
282
+ console.log(` Lessons: ${index.stats.totalLessons}`);
283
+ console.log(` Patterns: ${index.stats.totalPatterns}`);
284
+ console.log(` Tags: ${index.stats.totalTags}`);
285
+ }
286
+ return;
287
+ }
288
+
289
+ if (args.includes('--rebuild')) {
290
+ console.log('🔄 Rebuilding knowledge index...');
291
+ const index = rebuildIndex();
292
+ console.log(`✅ Index rebuilt successfully`);
293
+ console.log(` Lessons: ${index.stats.totalLessons}`);
294
+ console.log(` Patterns: ${index.stats.totalPatterns}`);
295
+ console.log(` Tags: ${index.stats.totalTags}`);
296
+ return;
297
+ }
298
+
299
+ console.log(`
300
+ 📇 Knowledge Index Manager
301
+
302
+ Usage:
303
+ agent index --rebuild Rebuild index from knowledge files
304
+ agent index --status Check if index is fresh or stale
305
+
306
+ The index provides O(1) lookup for:
307
+ - Pattern matching (by keyword)
308
+ - Tag filtering
309
+ - Severity grouping
310
+ - Direct ID lookup
311
+ `);
312
+ }
313
+
314
+ // Run if called directly
315
+ if (process.argv[1]?.includes('knowledge-index')) {
316
+ main();
317
+ }
318
+
319
+ export default {
320
+ loadIndex,
321
+ rebuildIndex,
322
+ checkIndexFreshness,
323
+ searchByPattern,
324
+ searchByTag,
325
+ getById
326
+ };
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knowledge Metrics Collector
4
+ *
5
+ * Aggregates KPIs from knowledge base for observability.
6
+ * Metrics are collected on-demand or on scan completion.
7
+ *
8
+ * Usage:
9
+ * agent metrics # Show dashboard
10
+ * agent metrics --json # JSON output for CI
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import yaml from 'js-yaml';
16
+ import { KNOWLEDGE_DIR } from './config.js';
17
+ import { loadIndex } from './knowledge-index.js';
18
+
19
+ const METRICS_PATH = path.join(KNOWLEDGE_DIR, 'metrics.json');
20
+
21
+ /**
22
+ * Load mistakes and improvements
23
+ * @returns {{ mistakes: Array, improvements: Array }}
24
+ */
25
+ function loadKnowledge() {
26
+ const mistakes = [];
27
+ const improvements = [];
28
+
29
+ const mistakesPath = path.join(KNOWLEDGE_DIR, 'mistakes.yaml');
30
+ if (fs.existsSync(mistakesPath)) {
31
+ const data = yaml.load(fs.readFileSync(mistakesPath, 'utf8'));
32
+ if (data?.mistakes) mistakes.push(...data.mistakes);
33
+ }
34
+
35
+ const improvementsPath = path.join(KNOWLEDGE_DIR, 'improvements.yaml');
36
+ if (fs.existsSync(improvementsPath)) {
37
+ const data = yaml.load(fs.readFileSync(improvementsPath, 'utf8'));
38
+ if (data?.improvements) improvements.push(...data.improvements);
39
+ }
40
+
41
+ return { mistakes, improvements };
42
+ }
43
+
44
+ /**
45
+ * Calculate average confidence score
46
+ * @param {Array} lessons
47
+ * @returns {number}
48
+ */
49
+ function calculateAvgConfidence(lessons) {
50
+ if (lessons.length === 0) return 0;
51
+
52
+ const total = lessons.reduce((sum, l) => {
53
+ return sum + (l.cognitive?.confidence || 0.3);
54
+ }, 0);
55
+
56
+ return total / lessons.length;
57
+ }
58
+
59
+ /**
60
+ * Calculate escalation rate
61
+ * @param {Array} mistakes
62
+ * @returns {number}
63
+ */
64
+ function calculateEscalationRate(mistakes) {
65
+ if (mistakes.length === 0) return 0;
66
+
67
+ const escalated = mistakes.filter(m => m.autoEscalated === true).length;
68
+ return escalated / mistakes.length;
69
+ }
70
+
71
+ /**
72
+ * Calculate fix rate
73
+ * @param {Array} improvements
74
+ * @returns {number}
75
+ */
76
+ function calculateFixRate(improvements) {
77
+ if (improvements.length === 0) return 0;
78
+
79
+ const applied = improvements.filter(i => (i.appliedCount || 0) > 0).length;
80
+ return applied / improvements.length;
81
+ }
82
+
83
+ /**
84
+ * Get 7-day hit trend
85
+ * @param {Array} lessons
86
+ * @returns {number[]}
87
+ */
88
+ function get7DayTrend(lessons) {
89
+ const now = Date.now();
90
+ const dayMs = 24 * 60 * 60 * 1000;
91
+ const trend = new Array(7).fill(0);
92
+
93
+ for (const lesson of lessons) {
94
+ if (lesson.lastHit) {
95
+ const hitTime = new Date(lesson.lastHit).getTime();
96
+ const daysAgo = Math.floor((now - hitTime) / dayMs);
97
+
98
+ if (daysAgo >= 0 && daysAgo < 7) {
99
+ trend[6 - daysAgo] += lesson.hitCount || 0;
100
+ }
101
+ }
102
+ }
103
+
104
+ return trend;
105
+ }
106
+
107
+ /**
108
+ * Get new lessons in last 30 days
109
+ * @param {Array} lessons
110
+ * @returns {number}
111
+ */
112
+ function getNewLessonsLast30Days(lessons) {
113
+ const now = Date.now();
114
+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
115
+
116
+ return lessons.filter(l => {
117
+ if (l.addedAt) {
118
+ const addedTime = new Date(l.addedAt).getTime();
119
+ return (now - addedTime) < thirtyDaysMs;
120
+ }
121
+ return false;
122
+ }).length;
123
+ }
124
+
125
+ /**
126
+ * Get top violations
127
+ * @param {Array} mistakes
128
+ * @param {number} limit
129
+ * @returns {Array}
130
+ */
131
+ function getTopViolations(mistakes, limit = 5) {
132
+ return mistakes
133
+ .map(m => ({
134
+ id: m.id,
135
+ pattern: m.pattern,
136
+ hitCount: m.hitCount || 0,
137
+ severity: m.severity || 'WARNING'
138
+ }))
139
+ .sort((a, b) => b.hitCount - a.hitCount)
140
+ .slice(0, limit);
141
+ }
142
+
143
+ /**
144
+ * Get stale lessons (no hits, old)
145
+ * @param {Array} lessons
146
+ * @returns {number}
147
+ */
148
+ function getStaleLessonsCount(lessons) {
149
+ const now = Date.now();
150
+ const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000;
151
+
152
+ return lessons.filter(l => {
153
+ const noHits = !l.hitCount || l.hitCount === 0;
154
+ const isOld = l.addedAt && (now - new Date(l.addedAt).getTime()) > ninetyDaysMs;
155
+ return noHits && isOld;
156
+ }).length;
157
+ }
158
+
159
+ /**
160
+ * Collect all metrics
161
+ * @returns {Object}
162
+ */
163
+ export function collectMetrics() {
164
+ const { mistakes, improvements } = loadKnowledge();
165
+ const allLessons = [...mistakes, ...improvements];
166
+
167
+ // Load index for additional stats
168
+ let indexStats = { totalPatterns: 0, totalTags: 0 };
169
+ try {
170
+ const index = loadIndex();
171
+ indexStats = index.stats || indexStats;
172
+ } catch (e) {
173
+ // Index may not exist yet
174
+ }
175
+
176
+ const metrics = {
177
+ version: 1,
178
+ lastUpdated: new Date().toISOString(),
179
+
180
+ kpis: {
181
+ totalLessons: allLessons.length,
182
+ totalMistakes: mistakes.length,
183
+ totalImprovements: improvements.length,
184
+ activePatterns: allLessons.filter(l => (l.hitCount || 0) > 0).length,
185
+ totalHits: allLessons.reduce((sum, l) => sum + (l.hitCount || 0), 0),
186
+ avgConfidence: parseFloat(calculateAvgConfidence(allLessons).toFixed(2)),
187
+ escalationRate: parseFloat(calculateEscalationRate(mistakes).toFixed(2)),
188
+ fixRate: parseFloat(calculateFixRate(improvements).toFixed(2)),
189
+ staleLessons: getStaleLessonsCount(allLessons)
190
+ },
191
+
192
+ trends: {
193
+ hitsLast7Days: get7DayTrend(allLessons),
194
+ newLessonsLast30Days: getNewLessonsLast30Days(allLessons)
195
+ },
196
+
197
+ topViolations: getTopViolations(mistakes, 5),
198
+
199
+ breakdown: {
200
+ bySeverity: {
201
+ ERROR: mistakes.filter(m => m.severity === 'ERROR').length,
202
+ WARNING: mistakes.filter(m => m.severity === 'WARNING').length,
203
+ INFO: improvements.length
204
+ },
205
+ byMaturity: {
206
+ stable: allLessons.filter(l => l.cognitive?.maturity === 'stable').length,
207
+ learning: allLessons.filter(l => l.cognitive?.maturity === 'learning').length,
208
+ deprecated: allLessons.filter(l => l.cognitive?.maturity === 'deprecated').length
209
+ }
210
+ },
211
+
212
+ index: indexStats
213
+ };
214
+
215
+ return metrics;
216
+ }
217
+
218
+ /**
219
+ * Save metrics to file
220
+ * @param {Object} metrics
221
+ */
222
+ export function saveMetrics(metrics) {
223
+ fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2), 'utf8');
224
+ }
225
+
226
+ /**
227
+ * Load cached metrics
228
+ * @returns {Object|null}
229
+ */
230
+ export function loadMetrics() {
231
+ if (!fs.existsSync(METRICS_PATH)) {
232
+ return null;
233
+ }
234
+
235
+ try {
236
+ return JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
237
+ } catch (e) {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Display metrics dashboard
244
+ * @param {Object} metrics
245
+ */
246
+ function displayDashboard(metrics) {
247
+ console.log(`
248
+ 📊 Knowledge Metrics Dashboard
249
+ ${'─'.repeat(50)}
250
+
251
+ 📈 KPIs:
252
+ Total Lessons: ${metrics.kpis.totalLessons}
253
+ Active Patterns: ${metrics.kpis.activePatterns} (${Math.round((metrics.kpis.activePatterns / metrics.kpis.totalLessons) * 100)}%)
254
+ Total Hits: ${metrics.kpis.totalHits}
255
+ Avg Confidence: ${metrics.kpis.avgConfidence}
256
+ Escalation Rate: ${Math.round(metrics.kpis.escalationRate * 100)}%
257
+ Fix Rate: ${Math.round(metrics.kpis.fixRate * 100)}%
258
+ Stale Lessons: ${metrics.kpis.staleLessons}
259
+
260
+ 📊 Breakdown:
261
+ Mistakes: ${metrics.kpis.totalMistakes}
262
+ ERROR: ${metrics.breakdown.bySeverity.ERROR}
263
+ WARNING: ${metrics.breakdown.bySeverity.WARNING}
264
+ Improvements: ${metrics.kpis.totalImprovements}
265
+
266
+ 🧠 Maturity:
267
+ Stable: ${metrics.breakdown.byMaturity.stable}
268
+ Learning: ${metrics.breakdown.byMaturity.learning}
269
+ Deprecated: ${metrics.breakdown.byMaturity.deprecated}
270
+
271
+ 📈 Trends:
272
+ 7-Day Hits: ${sparkline(metrics.trends.hitsLast7Days)}
273
+ New (30d): ${metrics.trends.newLessonsLast30Days}
274
+
275
+ 🔥 Top Violations:`);
276
+
277
+ metrics.topViolations.forEach((v, i) => {
278
+ console.log(` ${i + 1}. ${v.id}: ${v.hitCount} hits (${v.severity})`);
279
+ });
280
+
281
+ console.log(`
282
+ 🔎 Index:
283
+ Patterns: ${metrics.index.totalPatterns}
284
+ Tags: ${metrics.index.totalTags}
285
+
286
+ Last Updated: ${new Date(metrics.lastUpdated).toLocaleString()}
287
+ `);
288
+ }
289
+
290
+ /**
291
+ * Generate ASCII sparkline
292
+ * @param {number[]} data
293
+ * @returns {string}
294
+ */
295
+ function sparkline(data) {
296
+ const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
297
+ const max = Math.max(...data, 1);
298
+
299
+ return data.map(val => {
300
+ const index = Math.floor((val / max) * (chars.length - 1));
301
+ return chars[index];
302
+ }).join('');
303
+ }
304
+
305
+ /**
306
+ * CLI entry point
307
+ */
308
+ function main() {
309
+ const args = process.argv.slice(2);
310
+ const jsonMode = args.includes('--json');
311
+
312
+ console.log('🔄 Collecting metrics...');
313
+ const metrics = collectMetrics();
314
+
315
+ // Save for caching
316
+ saveMetrics(metrics);
317
+
318
+ if (jsonMode) {
319
+ console.log(JSON.stringify(metrics, null, 2));
320
+ return;
321
+ }
322
+
323
+ displayDashboard(metrics);
324
+ }
325
+
326
+ // Run if called directly
327
+ if (process.argv[1]?.includes('knowledge-metrics')) {
328
+ main();
329
+ }
330
+
331
+ export default {
332
+ collectMetrics,
333
+ saveMetrics,
334
+ loadMetrics
335
+ };