neuronlayer 0.1.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/CONTRIBUTING.md +127 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/index.js +38016 -0
- package/esbuild.config.js +26 -0
- package/package.json +63 -0
- package/src/cli/commands.ts +382 -0
- package/src/core/adr-exporter.ts +253 -0
- package/src/core/architecture/architecture-enforcement.ts +228 -0
- package/src/core/architecture/duplicate-detector.ts +288 -0
- package/src/core/architecture/index.ts +6 -0
- package/src/core/architecture/pattern-learner.ts +306 -0
- package/src/core/architecture/pattern-library.ts +403 -0
- package/src/core/architecture/pattern-validator.ts +324 -0
- package/src/core/change-intelligence/bug-correlator.ts +444 -0
- package/src/core/change-intelligence/change-intelligence.ts +221 -0
- package/src/core/change-intelligence/change-tracker.ts +334 -0
- package/src/core/change-intelligence/fix-suggester.ts +340 -0
- package/src/core/change-intelligence/index.ts +5 -0
- package/src/core/code-verifier.ts +843 -0
- package/src/core/confidence/confidence-scorer.ts +251 -0
- package/src/core/confidence/conflict-checker.ts +289 -0
- package/src/core/confidence/index.ts +5 -0
- package/src/core/confidence/source-tracker.ts +263 -0
- package/src/core/confidence/warning-detector.ts +241 -0
- package/src/core/context-rot/compaction.ts +284 -0
- package/src/core/context-rot/context-health.ts +243 -0
- package/src/core/context-rot/context-rot-prevention.ts +213 -0
- package/src/core/context-rot/critical-context.ts +221 -0
- package/src/core/context-rot/drift-detector.ts +255 -0
- package/src/core/context-rot/index.ts +7 -0
- package/src/core/context.ts +263 -0
- package/src/core/decision-extractor.ts +339 -0
- package/src/core/decisions.ts +69 -0
- package/src/core/deja-vu.ts +421 -0
- package/src/core/engine.ts +1455 -0
- package/src/core/feature-context.ts +726 -0
- package/src/core/ghost-mode.ts +412 -0
- package/src/core/learning.ts +485 -0
- package/src/core/living-docs/activity-tracker.ts +296 -0
- package/src/core/living-docs/architecture-generator.ts +428 -0
- package/src/core/living-docs/changelog-generator.ts +348 -0
- package/src/core/living-docs/component-generator.ts +230 -0
- package/src/core/living-docs/doc-engine.ts +110 -0
- package/src/core/living-docs/doc-validator.ts +282 -0
- package/src/core/living-docs/index.ts +8 -0
- package/src/core/project-manager.ts +297 -0
- package/src/core/summarizer.ts +267 -0
- package/src/core/test-awareness/change-validator.ts +499 -0
- package/src/core/test-awareness/index.ts +5 -0
- package/src/index.ts +49 -0
- package/src/indexing/ast.ts +563 -0
- package/src/indexing/embeddings.ts +85 -0
- package/src/indexing/indexer.ts +245 -0
- package/src/indexing/watcher.ts +78 -0
- package/src/server/gateways/aggregator.ts +374 -0
- package/src/server/gateways/index.ts +473 -0
- package/src/server/gateways/memory-ghost.ts +343 -0
- package/src/server/gateways/memory-query.ts +452 -0
- package/src/server/gateways/memory-record.ts +346 -0
- package/src/server/gateways/memory-review.ts +410 -0
- package/src/server/gateways/memory-status.ts +517 -0
- package/src/server/gateways/memory-verify.ts +392 -0
- package/src/server/gateways/router.ts +434 -0
- package/src/server/gateways/types.ts +610 -0
- package/src/server/mcp.ts +154 -0
- package/src/server/resources.ts +85 -0
- package/src/server/tools.ts +2261 -0
- package/src/storage/database.ts +262 -0
- package/src/storage/tier1.ts +135 -0
- package/src/storage/tier2.ts +764 -0
- package/src/storage/tier3.ts +123 -0
- package/src/types/documentation.ts +619 -0
- package/src/types/index.ts +222 -0
- package/src/utils/config.ts +193 -0
- package/src/utils/files.ts +117 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/tokens.ts +52 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
import type { SearchResult } from '../types/index.js';
|
|
4
|
+
|
|
5
|
+
export type UsageEventType =
|
|
6
|
+
| 'query' // User made a query
|
|
7
|
+
| 'file_view' // File was viewed
|
|
8
|
+
| 'context_used' // Context was included in response
|
|
9
|
+
| 'context_ignored' // Context was retrieved but not used
|
|
10
|
+
| 'decision_made' // User made a decision
|
|
11
|
+
| 'file_edit'; // User edited a file
|
|
12
|
+
|
|
13
|
+
interface UsageEvent {
|
|
14
|
+
eventType: UsageEventType;
|
|
15
|
+
filePath?: string;
|
|
16
|
+
query?: string;
|
|
17
|
+
contextUsed?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FileAccessStats {
|
|
21
|
+
fileId: number;
|
|
22
|
+
accessCount: number;
|
|
23
|
+
lastAccessed: number;
|
|
24
|
+
relevanceScore: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface QueryPattern {
|
|
28
|
+
queryHash: string;
|
|
29
|
+
queryText: string;
|
|
30
|
+
resultFiles: string[];
|
|
31
|
+
hitCount: number;
|
|
32
|
+
avgUsefulness: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class LearningEngine {
|
|
36
|
+
private db: Database.Database;
|
|
37
|
+
private hotCache: Map<string, { content: string; accessCount: number; lastAccessed: number }> = new Map();
|
|
38
|
+
private readonly MAX_CACHE_SIZE = 50;
|
|
39
|
+
private readonly CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
40
|
+
|
|
41
|
+
constructor(db: Database.Database) {
|
|
42
|
+
this.db = db;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Track usage events
|
|
46
|
+
trackEvent(event: UsageEvent): void {
|
|
47
|
+
const stmt = this.db.prepare(`
|
|
48
|
+
INSERT INTO usage_events (event_type, file_path, query, context_used, timestamp)
|
|
49
|
+
VALUES (?, ?, ?, ?, unixepoch())
|
|
50
|
+
`);
|
|
51
|
+
stmt.run(
|
|
52
|
+
event.eventType,
|
|
53
|
+
event.filePath || null,
|
|
54
|
+
event.query || null,
|
|
55
|
+
event.contextUsed ? 1 : 0
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Update file access stats if file was accessed
|
|
59
|
+
if (event.filePath && (event.eventType === 'file_view' || event.eventType === 'context_used')) {
|
|
60
|
+
this.updateFileAccess(event.filePath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Track a query and its results for pattern learning
|
|
65
|
+
trackQuery(query: string, resultFiles: string[]): void {
|
|
66
|
+
const queryHash = this.hashQuery(query);
|
|
67
|
+
|
|
68
|
+
const stmt = this.db.prepare(`
|
|
69
|
+
INSERT INTO query_patterns (query_hash, query_text, result_files, hit_count, last_used)
|
|
70
|
+
VALUES (?, ?, ?, 1, unixepoch())
|
|
71
|
+
ON CONFLICT(query_hash) DO UPDATE SET
|
|
72
|
+
hit_count = hit_count + 1,
|
|
73
|
+
last_used = unixepoch()
|
|
74
|
+
`);
|
|
75
|
+
stmt.run(queryHash, query, JSON.stringify(resultFiles));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update usefulness score for a query pattern
|
|
79
|
+
updateQueryUsefulness(query: string, wasUseful: boolean): void {
|
|
80
|
+
const queryHash = this.hashQuery(query);
|
|
81
|
+
|
|
82
|
+
// Exponential moving average for usefulness
|
|
83
|
+
const alpha = 0.3;
|
|
84
|
+
const newScore = wasUseful ? 1.0 : 0.0;
|
|
85
|
+
|
|
86
|
+
const stmt = this.db.prepare(`
|
|
87
|
+
UPDATE query_patterns
|
|
88
|
+
SET avg_usefulness = avg_usefulness * (1 - ?) + ? * ?
|
|
89
|
+
WHERE query_hash = ?
|
|
90
|
+
`);
|
|
91
|
+
stmt.run(alpha, alpha, newScore, queryHash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Update file access statistics
|
|
95
|
+
private updateFileAccess(filePath: string): void {
|
|
96
|
+
// Get file ID
|
|
97
|
+
const fileStmt = this.db.prepare('SELECT id FROM files WHERE path = ?');
|
|
98
|
+
const file = fileStmt.get(filePath) as { id: number } | undefined;
|
|
99
|
+
|
|
100
|
+
if (!file) return;
|
|
101
|
+
|
|
102
|
+
const stmt = this.db.prepare(`
|
|
103
|
+
INSERT INTO file_access (file_id, access_count, last_accessed, relevance_score)
|
|
104
|
+
VALUES (?, 1, unixepoch(), 0.5)
|
|
105
|
+
ON CONFLICT(file_id) DO UPDATE SET
|
|
106
|
+
access_count = access_count + 1,
|
|
107
|
+
last_accessed = unixepoch(),
|
|
108
|
+
relevance_score = MIN(1.0, relevance_score + 0.05)
|
|
109
|
+
`);
|
|
110
|
+
stmt.run(file.id);
|
|
111
|
+
|
|
112
|
+
// Update hot cache
|
|
113
|
+
this.updateHotCache(filePath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get personalized boost for a file based on usage patterns
|
|
117
|
+
getPersonalizedBoost(filePath: string): number {
|
|
118
|
+
const fileStmt = this.db.prepare('SELECT id FROM files WHERE path = ?');
|
|
119
|
+
const file = fileStmt.get(filePath) as { id: number } | undefined;
|
|
120
|
+
|
|
121
|
+
if (!file) return 1.0;
|
|
122
|
+
|
|
123
|
+
const stmt = this.db.prepare(`
|
|
124
|
+
SELECT access_count, last_accessed, relevance_score
|
|
125
|
+
FROM file_access
|
|
126
|
+
WHERE file_id = ?
|
|
127
|
+
`);
|
|
128
|
+
const stats = stmt.get(file.id) as FileAccessStats | undefined;
|
|
129
|
+
|
|
130
|
+
if (!stats) return 1.0;
|
|
131
|
+
|
|
132
|
+
// Calculate boost based on:
|
|
133
|
+
// 1. Access frequency (log scale to avoid extreme boosts)
|
|
134
|
+
// 2. Recency (decay over time)
|
|
135
|
+
// 3. Learned relevance score
|
|
136
|
+
|
|
137
|
+
const frequencyBoost = 1 + Math.log10(1 + stats.accessCount) * 0.2;
|
|
138
|
+
|
|
139
|
+
const hoursSinceAccess = (Date.now() / 1000 - stats.lastAccessed) / 3600;
|
|
140
|
+
const recencyBoost = Math.exp(-hoursSinceAccess / 168); // Decay over 1 week
|
|
141
|
+
|
|
142
|
+
const relevanceBoost = 0.5 + stats.relevanceScore;
|
|
143
|
+
|
|
144
|
+
return frequencyBoost * (0.7 + 0.3 * recencyBoost) * relevanceBoost;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply personalized ranking to search results
|
|
148
|
+
applyPersonalizedRanking(results: SearchResult[]): SearchResult[] {
|
|
149
|
+
return results
|
|
150
|
+
.map(r => ({
|
|
151
|
+
...r,
|
|
152
|
+
score: (r.score || r.similarity) * this.getPersonalizedBoost(r.file)
|
|
153
|
+
}))
|
|
154
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get frequently accessed files for pre-fetching
|
|
158
|
+
getFrequentFiles(limit: number = 20): string[] {
|
|
159
|
+
const stmt = this.db.prepare(`
|
|
160
|
+
SELECT f.path
|
|
161
|
+
FROM file_access fa
|
|
162
|
+
JOIN files f ON fa.file_id = f.id
|
|
163
|
+
ORDER BY fa.access_count DESC, fa.last_accessed DESC
|
|
164
|
+
LIMIT ?
|
|
165
|
+
`);
|
|
166
|
+
const rows = stmt.all(limit) as { path: string }[];
|
|
167
|
+
return rows.map(r => r.path);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Get recently accessed files
|
|
171
|
+
getRecentFiles(limit: number = 10): string[] {
|
|
172
|
+
const stmt = this.db.prepare(`
|
|
173
|
+
SELECT f.path
|
|
174
|
+
FROM file_access fa
|
|
175
|
+
JOIN files f ON fa.file_id = f.id
|
|
176
|
+
ORDER BY fa.last_accessed DESC
|
|
177
|
+
LIMIT ?
|
|
178
|
+
`);
|
|
179
|
+
const rows = stmt.all(limit) as { path: string }[];
|
|
180
|
+
return rows.map(r => r.path);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Predict likely needed files based on current context
|
|
184
|
+
predictNeededFiles(currentFile: string, query: string): string[] {
|
|
185
|
+
const predictions: Set<string> = new Set();
|
|
186
|
+
|
|
187
|
+
// 1. Files frequently accessed together with current file
|
|
188
|
+
const coAccessStmt = this.db.prepare(`
|
|
189
|
+
SELECT DISTINCT ue2.file_path
|
|
190
|
+
FROM usage_events ue1
|
|
191
|
+
JOIN usage_events ue2 ON ABS(ue1.timestamp - ue2.timestamp) < 300
|
|
192
|
+
WHERE ue1.file_path = ?
|
|
193
|
+
AND ue2.file_path != ?
|
|
194
|
+
AND ue2.file_path IS NOT NULL
|
|
195
|
+
GROUP BY ue2.file_path
|
|
196
|
+
ORDER BY COUNT(*) DESC
|
|
197
|
+
LIMIT 5
|
|
198
|
+
`);
|
|
199
|
+
const coAccessed = coAccessStmt.all(currentFile, currentFile) as { file_path: string }[];
|
|
200
|
+
coAccessed.forEach(r => predictions.add(r.file_path));
|
|
201
|
+
|
|
202
|
+
// 2. Files from similar past queries
|
|
203
|
+
const queryHash = this.hashQuery(query);
|
|
204
|
+
const similarQueryStmt = this.db.prepare(`
|
|
205
|
+
SELECT result_files
|
|
206
|
+
FROM query_patterns
|
|
207
|
+
WHERE query_hash = ?
|
|
208
|
+
OR query_text LIKE ?
|
|
209
|
+
ORDER BY avg_usefulness DESC, hit_count DESC
|
|
210
|
+
LIMIT 3
|
|
211
|
+
`);
|
|
212
|
+
const keywords = query.split(/\s+/).slice(0, 3).join('%');
|
|
213
|
+
const similarQueries = similarQueryStmt.all(queryHash, `%${keywords}%`) as { result_files: string }[];
|
|
214
|
+
|
|
215
|
+
for (const q of similarQueries) {
|
|
216
|
+
try {
|
|
217
|
+
const files = JSON.parse(q.result_files) as string[];
|
|
218
|
+
files.slice(0, 3).forEach(f => predictions.add(f));
|
|
219
|
+
} catch {
|
|
220
|
+
// Invalid JSON, skip
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 3. Files that import/are imported by current file
|
|
225
|
+
const depStmt = this.db.prepare(`
|
|
226
|
+
SELECT DISTINCT f2.path
|
|
227
|
+
FROM files f1
|
|
228
|
+
JOIN imports i ON i.file_id = f1.id
|
|
229
|
+
JOIN files f2 ON f2.path LIKE '%' || REPLACE(i.imported_from, './', '') || '%'
|
|
230
|
+
WHERE f1.path = ?
|
|
231
|
+
LIMIT 5
|
|
232
|
+
`);
|
|
233
|
+
const deps = depStmt.all(currentFile) as { path: string }[];
|
|
234
|
+
deps.forEach(r => predictions.add(r.path));
|
|
235
|
+
|
|
236
|
+
return Array.from(predictions).slice(0, 10);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Hot cache management
|
|
240
|
+
private updateHotCache(filePath: string): void {
|
|
241
|
+
const existing = this.hotCache.get(filePath);
|
|
242
|
+
|
|
243
|
+
if (existing) {
|
|
244
|
+
existing.accessCount++;
|
|
245
|
+
existing.lastAccessed = Date.now();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Evict old entries if cache is full
|
|
249
|
+
if (this.hotCache.size >= this.MAX_CACHE_SIZE) {
|
|
250
|
+
this.evictCacheEntries();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private evictCacheEntries(): void {
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
const entries = Array.from(this.hotCache.entries());
|
|
257
|
+
|
|
258
|
+
// Remove expired entries
|
|
259
|
+
for (const [key, value] of entries) {
|
|
260
|
+
if (now - value.lastAccessed > this.CACHE_TTL_MS) {
|
|
261
|
+
this.hotCache.delete(key);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// If still too full, remove least accessed
|
|
266
|
+
if (this.hotCache.size >= this.MAX_CACHE_SIZE) {
|
|
267
|
+
entries
|
|
268
|
+
.sort((a, b) => a[1].accessCount - b[1].accessCount)
|
|
269
|
+
.slice(0, 10)
|
|
270
|
+
.forEach(([key]) => this.hotCache.delete(key));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
addToHotCache(filePath: string, content: string): void {
|
|
275
|
+
this.hotCache.set(filePath, {
|
|
276
|
+
content,
|
|
277
|
+
accessCount: 1,
|
|
278
|
+
lastAccessed: Date.now()
|
|
279
|
+
});
|
|
280
|
+
this.updateHotCache(filePath);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getFromHotCache(filePath: string): string | null {
|
|
284
|
+
const entry = this.hotCache.get(filePath);
|
|
285
|
+
if (entry) {
|
|
286
|
+
entry.accessCount++;
|
|
287
|
+
entry.lastAccessed = Date.now();
|
|
288
|
+
return entry.content;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
isInHotCache(filePath: string): boolean {
|
|
294
|
+
return this.hotCache.has(filePath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getHotCacheStats(): { size: number; files: string[] } {
|
|
298
|
+
return {
|
|
299
|
+
size: this.hotCache.size,
|
|
300
|
+
files: Array.from(this.hotCache.keys())
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Query expansion for better retrieval
|
|
305
|
+
expandQuery(query: string): string[] {
|
|
306
|
+
const expansions: string[] = [query];
|
|
307
|
+
const words = query.toLowerCase().split(/\s+/);
|
|
308
|
+
|
|
309
|
+
// Add variations based on common patterns
|
|
310
|
+
const synonyms: Record<string, string[]> = {
|
|
311
|
+
'auth': ['authentication', 'login', 'authorization'],
|
|
312
|
+
'db': ['database', 'sql', 'storage'],
|
|
313
|
+
'api': ['endpoint', 'route', 'handler'],
|
|
314
|
+
'ui': ['component', 'view', 'frontend'],
|
|
315
|
+
'error': ['exception', 'failure', 'bug'],
|
|
316
|
+
'test': ['spec', 'unit', 'integration'],
|
|
317
|
+
'config': ['configuration', 'settings', 'options'],
|
|
318
|
+
'user': ['account', 'profile', 'member'],
|
|
319
|
+
'fix': ['bug', 'issue', 'problem'],
|
|
320
|
+
'add': ['create', 'implement', 'new'],
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Expand synonyms
|
|
324
|
+
for (const word of words) {
|
|
325
|
+
const syns = synonyms[word];
|
|
326
|
+
if (syns) {
|
|
327
|
+
for (const syn of syns) {
|
|
328
|
+
expansions.push(query.replace(new RegExp(word, 'gi'), syn));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Add partial queries for broader search
|
|
334
|
+
if (words.length > 2) {
|
|
335
|
+
expansions.push(words.slice(0, 2).join(' '));
|
|
336
|
+
expansions.push(words.slice(-2).join(' '));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return [...new Set(expansions)].slice(0, 5);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Get usage statistics
|
|
343
|
+
getUsageStats(): {
|
|
344
|
+
totalQueries: number;
|
|
345
|
+
totalFileViews: number;
|
|
346
|
+
topFiles: Array<{ path: string; count: number }>;
|
|
347
|
+
recentActivity: number;
|
|
348
|
+
} {
|
|
349
|
+
const queryCountStmt = this.db.prepare(`
|
|
350
|
+
SELECT COUNT(*) as count FROM usage_events WHERE event_type = 'query'
|
|
351
|
+
`);
|
|
352
|
+
const totalQueries = (queryCountStmt.get() as { count: number }).count;
|
|
353
|
+
|
|
354
|
+
const viewCountStmt = this.db.prepare(`
|
|
355
|
+
SELECT COUNT(*) as count FROM usage_events WHERE event_type = 'file_view'
|
|
356
|
+
`);
|
|
357
|
+
const totalFileViews = (viewCountStmt.get() as { count: number }).count;
|
|
358
|
+
|
|
359
|
+
const topFilesStmt = this.db.prepare(`
|
|
360
|
+
SELECT f.path, fa.access_count as count
|
|
361
|
+
FROM file_access fa
|
|
362
|
+
JOIN files f ON fa.file_id = f.id
|
|
363
|
+
ORDER BY fa.access_count DESC
|
|
364
|
+
LIMIT 10
|
|
365
|
+
`);
|
|
366
|
+
const topFiles = topFilesStmt.all() as Array<{ path: string; count: number }>;
|
|
367
|
+
|
|
368
|
+
const recentStmt = this.db.prepare(`
|
|
369
|
+
SELECT COUNT(*) as count
|
|
370
|
+
FROM usage_events
|
|
371
|
+
WHERE timestamp > unixepoch() - 3600
|
|
372
|
+
`);
|
|
373
|
+
const recentActivity = (recentStmt.get() as { count: number }).count;
|
|
374
|
+
|
|
375
|
+
return { totalQueries, totalFileViews, topFiles, recentActivity };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clean up old usage data
|
|
379
|
+
cleanup(daysToKeep: number = 30): number {
|
|
380
|
+
const stmt = this.db.prepare(`
|
|
381
|
+
DELETE FROM usage_events
|
|
382
|
+
WHERE timestamp < unixepoch() - ? * 86400
|
|
383
|
+
`);
|
|
384
|
+
const result = stmt.run(daysToKeep);
|
|
385
|
+
return result.changes;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Update importance scores for all files based on usage patterns
|
|
390
|
+
* Called by background intelligence loop
|
|
391
|
+
*/
|
|
392
|
+
updateImportanceScores(): void {
|
|
393
|
+
try {
|
|
394
|
+
// Get all files with access stats
|
|
395
|
+
const stmt = this.db.prepare(`
|
|
396
|
+
SELECT f.id, f.path, fa.access_count, fa.last_accessed, fa.relevance_score
|
|
397
|
+
FROM files f
|
|
398
|
+
LEFT JOIN file_access fa ON fa.file_id = f.id
|
|
399
|
+
`);
|
|
400
|
+
|
|
401
|
+
const files = stmt.all() as Array<{
|
|
402
|
+
id: number;
|
|
403
|
+
path: string;
|
|
404
|
+
access_count: number | null;
|
|
405
|
+
last_accessed: number | null;
|
|
406
|
+
relevance_score: number | null;
|
|
407
|
+
}>;
|
|
408
|
+
|
|
409
|
+
// Calculate and update importance scores
|
|
410
|
+
const updateStmt = this.db.prepare(`
|
|
411
|
+
INSERT INTO file_access (file_id, access_count, last_accessed, relevance_score)
|
|
412
|
+
VALUES (?, COALESCE(?, 0), COALESCE(?, unixepoch()), ?)
|
|
413
|
+
ON CONFLICT(file_id) DO UPDATE SET
|
|
414
|
+
relevance_score = excluded.relevance_score
|
|
415
|
+
`);
|
|
416
|
+
|
|
417
|
+
for (const file of files) {
|
|
418
|
+
const importance = this.calculateImportance(
|
|
419
|
+
file.access_count || 0,
|
|
420
|
+
file.last_accessed || Math.floor(Date.now() / 1000),
|
|
421
|
+
file.path
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
updateStmt.run(
|
|
425
|
+
file.id,
|
|
426
|
+
file.access_count,
|
|
427
|
+
file.last_accessed,
|
|
428
|
+
importance
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error('Error updating importance scores:', error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Calculate importance score for a file based on multiple factors
|
|
438
|
+
*/
|
|
439
|
+
calculateImportance(accessCount: number, lastAccessed: number, filePath: string): number {
|
|
440
|
+
// Factor 1: Access frequency (log scale to avoid extreme boosts)
|
|
441
|
+
const frequencyScore = Math.log10(1 + accessCount) * 0.4;
|
|
442
|
+
|
|
443
|
+
// Factor 2: Recency (how recently was it accessed)
|
|
444
|
+
const hoursSinceAccess = (Date.now() / 1000 - lastAccessed) / 3600;
|
|
445
|
+
const recencyScore = Math.exp(-hoursSinceAccess / 168) * 0.3; // Decay over 1 week
|
|
446
|
+
|
|
447
|
+
// Factor 3: File importance heuristics
|
|
448
|
+
let fileImportance = 0.3; // Default
|
|
449
|
+
|
|
450
|
+
// Boost important file types
|
|
451
|
+
if (filePath.includes('index.') || filePath.includes('main.')) {
|
|
452
|
+
fileImportance = 0.5;
|
|
453
|
+
} else if (filePath.includes('.config.') || filePath.includes('config/')) {
|
|
454
|
+
fileImportance = 0.45;
|
|
455
|
+
} else if (filePath.includes('.test.') || filePath.includes('.spec.')) {
|
|
456
|
+
fileImportance = 0.25; // Tests slightly less important for context
|
|
457
|
+
} else if (filePath.includes('/types/') || filePath.includes('.d.ts')) {
|
|
458
|
+
fileImportance = 0.4; // Type definitions are often helpful
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Combine factors (max 1.0)
|
|
462
|
+
return Math.min(1.0, frequencyScore + recencyScore + fileImportance);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get importance score for a specific file
|
|
467
|
+
*/
|
|
468
|
+
getFileImportance(filePath: string): number {
|
|
469
|
+
const stmt = this.db.prepare(`
|
|
470
|
+
SELECT fa.relevance_score
|
|
471
|
+
FROM files f
|
|
472
|
+
JOIN file_access fa ON fa.file_id = f.id
|
|
473
|
+
WHERE f.path = ?
|
|
474
|
+
`);
|
|
475
|
+
|
|
476
|
+
const result = stmt.get(filePath) as { relevance_score: number } | undefined;
|
|
477
|
+
return result?.relevance_score || 0.5;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private hashQuery(query: string): string {
|
|
481
|
+
// Normalize query for matching
|
|
482
|
+
const normalized = query.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
483
|
+
return createHash('md5').update(normalized).digest('hex').slice(0, 16);
|
|
484
|
+
}
|
|
485
|
+
}
|