neuronlayer 0.1.3 → 0.1.4
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.
Potentially problematic release.
This version of neuronlayer might be problematic. Click here for more details.
- package/dist/index.js +618 -22
- package/package.json +3 -12
- package/src/core/change-intelligence/bug-correlator.ts +100 -0
- package/src/core/change-intelligence/change-intelligence.ts +43 -0
- package/src/core/engine.ts +173 -32
- package/src/core/learning.ts +34 -0
- package/src/core/living-docs/doc-engine.ts +24 -0
- package/src/core/refresh/activity-gate.ts +256 -0
- package/src/core/refresh/git-staleness-checker.ts +108 -0
- package/src/core/refresh/index.ts +27 -0
- package/src/core/summarizer.ts +23 -0
- package/src/server/tools.ts +70 -0
- package/src/storage/database.ts +9 -0
- package/CONTRIBUTING.md +0 -127
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuronlayer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Persistent memory layer for AI coding assistants - MCP server that makes AI truly understand your codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,16 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "node esbuild.config.js",
|
|
12
|
-
"build:benchmark": "node esbuild.benchmark.cjs",
|
|
13
|
-
"build:all": "npm run build && npm run build:benchmark",
|
|
14
12
|
"dev": "node --watch dist/index.js",
|
|
15
13
|
"start": "node dist/index.js",
|
|
16
|
-
"typecheck": "tsc --noEmit"
|
|
17
|
-
"test": "vitest",
|
|
18
|
-
"test:run": "vitest run",
|
|
19
|
-
"benchmark": "npm run build:all && node dist/benchmark/working-benchmark.js",
|
|
20
|
-
"benchmark:quick": "npm run build:all && node dist/benchmark/working-benchmark.js --quick",
|
|
21
|
-
"benchmark:full": "npm run build:all && node dist/benchmark/working-benchmark.js --iterations 50"
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
22
15
|
},
|
|
23
16
|
"keywords": [
|
|
24
17
|
"mcp",
|
|
@@ -28,7 +21,6 @@
|
|
|
28
21
|
"codebase",
|
|
29
22
|
"claude",
|
|
30
23
|
"embeddings",
|
|
31
|
-
"agent",
|
|
32
24
|
"coding-assistant",
|
|
33
25
|
"llm"
|
|
34
26
|
],
|
|
@@ -54,8 +46,7 @@
|
|
|
54
46
|
"@types/better-sqlite3": "^7.6.0",
|
|
55
47
|
"@types/node": "^22.0.0",
|
|
56
48
|
"esbuild": "^0.25.0",
|
|
57
|
-
"typescript": "^5.7.0"
|
|
58
|
-
"vitest": "^3.0.0"
|
|
49
|
+
"typescript": "^5.7.0"
|
|
59
50
|
},
|
|
60
51
|
"engines": {
|
|
61
52
|
"node": ">=18.0.0"
|
|
@@ -413,6 +413,106 @@ export class BugCorrelator {
|
|
|
413
413
|
return null;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Scan git history for fix commits and auto-record as bugs
|
|
418
|
+
* Looks for commits with "fix:", "bugfix:", "hotfix:" prefixes or "fixes #" references
|
|
419
|
+
*/
|
|
420
|
+
scanForBugFixes(): number {
|
|
421
|
+
try {
|
|
422
|
+
// Get commits from change_history that look like bug fixes
|
|
423
|
+
const fixCommits = this.db.prepare(`
|
|
424
|
+
SELECT DISTINCT commit_hash, commit_message, file, diff, timestamp
|
|
425
|
+
FROM change_history
|
|
426
|
+
WHERE (
|
|
427
|
+
LOWER(commit_message) LIKE 'fix:%' OR
|
|
428
|
+
LOWER(commit_message) LIKE 'fix(%' OR
|
|
429
|
+
LOWER(commit_message) LIKE 'bugfix:%' OR
|
|
430
|
+
LOWER(commit_message) LIKE 'hotfix:%' OR
|
|
431
|
+
LOWER(commit_message) LIKE '%fixes #%' OR
|
|
432
|
+
LOWER(commit_message) LIKE '%fixed #%' OR
|
|
433
|
+
LOWER(commit_message) LIKE '%closes #%'
|
|
434
|
+
)
|
|
435
|
+
ORDER BY timestamp DESC
|
|
436
|
+
LIMIT 50
|
|
437
|
+
`).all() as Array<{
|
|
438
|
+
commit_hash: string;
|
|
439
|
+
commit_message: string;
|
|
440
|
+
file: string;
|
|
441
|
+
diff: string | null;
|
|
442
|
+
timestamp: number;
|
|
443
|
+
}>;
|
|
444
|
+
|
|
445
|
+
let recorded = 0;
|
|
446
|
+
|
|
447
|
+
for (const commit of fixCommits) {
|
|
448
|
+
// Check if we already have this bug recorded (by commit hash in related_changes)
|
|
449
|
+
const existing = this.db.prepare(`
|
|
450
|
+
SELECT id FROM bug_history
|
|
451
|
+
WHERE fixed_by = ? OR related_changes LIKE ?
|
|
452
|
+
`).get(commit.commit_hash, `%${commit.commit_hash}%`);
|
|
453
|
+
|
|
454
|
+
if (existing) continue;
|
|
455
|
+
|
|
456
|
+
// Extract the bug description from commit message
|
|
457
|
+
const errorDescription = this.extractErrorFromCommitMessage(commit.commit_message);
|
|
458
|
+
|
|
459
|
+
// Record the bug as already fixed
|
|
460
|
+
const id = randomUUID();
|
|
461
|
+
|
|
462
|
+
this.db.prepare(`
|
|
463
|
+
INSERT INTO bug_history (id, error, file, timestamp, status, fixed_by, fixed_at, fix_diff, cause)
|
|
464
|
+
VALUES (?, ?, ?, ?, 'fixed', ?, ?, ?, ?)
|
|
465
|
+
`).run(
|
|
466
|
+
id,
|
|
467
|
+
errorDescription,
|
|
468
|
+
commit.file,
|
|
469
|
+
commit.timestamp,
|
|
470
|
+
commit.commit_hash,
|
|
471
|
+
commit.timestamp,
|
|
472
|
+
commit.diff?.slice(0, 2000) || null,
|
|
473
|
+
commit.commit_message
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
recorded++;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return recorded;
|
|
480
|
+
} catch {
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Extract a bug/error description from a fix commit message
|
|
487
|
+
*/
|
|
488
|
+
private extractErrorFromCommitMessage(message: string): string {
|
|
489
|
+
// Remove common prefixes
|
|
490
|
+
let cleaned = message
|
|
491
|
+
.replace(/^fix\s*[:\(]/i, '')
|
|
492
|
+
.replace(/^bugfix\s*[:\(]/i, '')
|
|
493
|
+
.replace(/^hotfix\s*[:\(]/i, '')
|
|
494
|
+
.replace(/\):\s*/, ': ')
|
|
495
|
+
.trim();
|
|
496
|
+
|
|
497
|
+
// Extract issue references
|
|
498
|
+
const issueMatch = cleaned.match(/(?:fixes|fixed|closes)\s*#(\d+)/i);
|
|
499
|
+
if (issueMatch) {
|
|
500
|
+
cleaned = cleaned.replace(/(?:fixes|fixed|closes)\s*#\d+/gi, '').trim();
|
|
501
|
+
if (cleaned) {
|
|
502
|
+
cleaned = `Issue #${issueMatch[1]}: ${cleaned}`;
|
|
503
|
+
} else {
|
|
504
|
+
cleaned = `Issue #${issueMatch[1]}`;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Capitalize first letter
|
|
509
|
+
if (cleaned.length > 0) {
|
|
510
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return cleaned || message;
|
|
514
|
+
}
|
|
515
|
+
|
|
416
516
|
// Get bug statistics
|
|
417
517
|
getBugStats(): {
|
|
418
518
|
total: number;
|
|
@@ -19,6 +19,8 @@ export class ChangeIntelligence {
|
|
|
19
19
|
private bugCorrelator: BugCorrelator;
|
|
20
20
|
private fixSuggester: FixSuggester;
|
|
21
21
|
private initialized = false;
|
|
22
|
+
private lastKnownHead: string | null = null;
|
|
23
|
+
private lastBugScanHead: string | null = null;
|
|
22
24
|
|
|
23
25
|
constructor(
|
|
24
26
|
projectPath: string,
|
|
@@ -40,6 +42,39 @@ export class ChangeIntelligence {
|
|
|
40
42
|
return synced;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// Sync recent git changes on-demand (event-driven alternative to polling)
|
|
46
|
+
syncFromGit(limit: number = 20): number {
|
|
47
|
+
return this.changeTracker.syncFromGit(limit);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the last known HEAD commit
|
|
52
|
+
*/
|
|
53
|
+
getLastKnownHead(): string | null {
|
|
54
|
+
return this.lastKnownHead;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update the last known HEAD (call this after syncing)
|
|
59
|
+
*/
|
|
60
|
+
setLastKnownHead(head: string): void {
|
|
61
|
+
this.lastKnownHead = head;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the last HEAD where we scanned for bug fixes
|
|
66
|
+
*/
|
|
67
|
+
getLastBugScanHead(): string | null {
|
|
68
|
+
return this.lastBugScanHead;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update the last bug scan HEAD (call this after scanning)
|
|
73
|
+
*/
|
|
74
|
+
setLastBugScanHead(head: string): void {
|
|
75
|
+
this.lastBugScanHead = head;
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
// Query what changed
|
|
44
79
|
whatChanged(options: ChangeQueryOptions = {}): ChangeQueryResult {
|
|
45
80
|
return this.changeTracker.queryChanges(options);
|
|
@@ -85,6 +120,14 @@ export class ChangeIntelligence {
|
|
|
85
120
|
return this.bugCorrelator.recordFix(bugId, fixDiff, cause);
|
|
86
121
|
}
|
|
87
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Scan git history for fix commits and auto-record as bugs
|
|
125
|
+
* Called by background intelligence loop
|
|
126
|
+
*/
|
|
127
|
+
scanForBugFixes(): number {
|
|
128
|
+
return this.bugCorrelator.scanForBugFixes();
|
|
129
|
+
}
|
|
130
|
+
|
|
88
131
|
// Search changes by keyword
|
|
89
132
|
searchChanges(keyword: string, limit?: number): Change[] {
|
|
90
133
|
return this.changeTracker.searchChanges(keyword, limit);
|
package/src/core/engine.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { TestAwareness } from './test-awareness/index.js';
|
|
|
22
22
|
import { GhostMode, type GhostInsight, type ConflictWarning } from './ghost-mode.js';
|
|
23
23
|
import { DejaVuDetector, type DejaVuMatch } from './deja-vu.js';
|
|
24
24
|
import { CodeVerifier, type VerificationResult, type VerificationCheck, type ImportVerification, type SecurityScanResult, type DependencyCheckResult } from './code-verifier.js';
|
|
25
|
+
import { GitStalenessChecker, ActivityGate } from './refresh/index.js';
|
|
25
26
|
import { detectLanguage, getPreview, countLines } from '../utils/files.js';
|
|
26
27
|
import type { MemoryLayerConfig, AssembledContext, Decision, ProjectSummary, SearchResult, CodeSymbol, SymbolKind, ActiveFeatureContext, HotContext } from '../types/index.js';
|
|
27
28
|
import type { ArchitectureDoc, ComponentDoc, DailyChangelog, ChangelogOptions, ValidationResult, ActivityResult, UndocumentedItem, ContextHealth, CompactionResult, CompactionOptions, CriticalContext, DriftResult, ConfidenceResult, ConfidenceLevel, ConfidenceSources, ConflictResult, ChangeQueryResult, ChangeQueryOptions, Diagnosis, PastBug, FixSuggestion, Change, Pattern, PatternCategory, PatternValidationResult, ExistingFunction, TestInfo, TestFramework, TestValidationResult, TestUpdate, TestCoverage } from '../types/documentation.js';
|
|
@@ -53,14 +54,12 @@ export class MemoryLayerEngine {
|
|
|
53
54
|
private ghostMode: GhostMode;
|
|
54
55
|
private dejaVu: DejaVuDetector;
|
|
55
56
|
private codeVerifier: CodeVerifier;
|
|
56
|
-
private
|
|
57
|
+
private gitStalenessChecker: GitStalenessChecker;
|
|
58
|
+
private activityGate: ActivityGate;
|
|
57
59
|
private initialized = false;
|
|
58
60
|
private initializationStatus: 'pending' | 'indexing' | 'ready' | 'error' = 'pending';
|
|
59
61
|
private indexingProgress: { indexed: number; total: number } = { indexed: 0, total: 0 };
|
|
60
62
|
|
|
61
|
-
// Background intelligence settings
|
|
62
|
-
private readonly BACKGROUND_REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
63
|
-
|
|
64
63
|
constructor(config: MemoryLayerConfig) {
|
|
65
64
|
this.config = config;
|
|
66
65
|
|
|
@@ -165,6 +164,10 @@ export class MemoryLayerEngine {
|
|
|
165
164
|
// Phase 13: Initialize Code Verifier (pre-commit quality gate)
|
|
166
165
|
this.codeVerifier = new CodeVerifier(config.projectPath);
|
|
167
166
|
|
|
167
|
+
// Intelligent Refresh System
|
|
168
|
+
this.gitStalenessChecker = new GitStalenessChecker(config.projectPath);
|
|
169
|
+
this.activityGate = new ActivityGate();
|
|
170
|
+
|
|
168
171
|
// Register this project
|
|
169
172
|
const projectInfo = this.projectManager.registerProject(config.projectPath);
|
|
170
173
|
this.projectManager.setActiveProject(projectInfo.id);
|
|
@@ -197,6 +200,13 @@ export class MemoryLayerEngine {
|
|
|
197
200
|
|
|
198
201
|
if (stats.indexed > 0) {
|
|
199
202
|
console.error(`Indexing complete: ${stats.indexed} files indexed`);
|
|
203
|
+
// Log activity for indexing
|
|
204
|
+
this.livingDocs.getActivityTracker().logActivity(
|
|
205
|
+
'indexing_complete',
|
|
206
|
+
`Indexed ${stats.indexed} files`,
|
|
207
|
+
undefined,
|
|
208
|
+
{ total: stats.total, indexed: stats.indexed }
|
|
209
|
+
);
|
|
200
210
|
} else {
|
|
201
211
|
console.error(`Index up to date (${stats.total} files)`);
|
|
202
212
|
}
|
|
@@ -209,6 +219,9 @@ export class MemoryLayerEngine {
|
|
|
209
219
|
this.indexer.on('fileIndexed', (path) => {
|
|
210
220
|
// Track file in feature context
|
|
211
221
|
this.featureContextManager.onFileOpened(path);
|
|
222
|
+
|
|
223
|
+
// Invalidate cached summary when file changes (event-driven, not polling)
|
|
224
|
+
this.summarizer.invalidateSummaryByPath(path);
|
|
212
225
|
});
|
|
213
226
|
|
|
214
227
|
this.indexer.on('error', (error) => {
|
|
@@ -247,8 +260,17 @@ export class MemoryLayerEngine {
|
|
|
247
260
|
console.error(`Test awareness: ${testResult.testsIndexed} tests indexed (${testResult.framework})`);
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
//
|
|
251
|
-
this.
|
|
263
|
+
// Scan for bug fixes from git history (one-time on init, not polling)
|
|
264
|
+
this.changeIntelligence.scanForBugFixes();
|
|
265
|
+
|
|
266
|
+
// Initialize git staleness checker with current HEAD
|
|
267
|
+
this.gitStalenessChecker.updateCachedHead();
|
|
268
|
+
|
|
269
|
+
// Register idle-time maintenance tasks
|
|
270
|
+
this.registerIdleTasks();
|
|
271
|
+
|
|
272
|
+
// Start idle monitoring
|
|
273
|
+
this.activityGate.startIdleMonitoring(10_000);
|
|
252
274
|
|
|
253
275
|
this.initialized = true;
|
|
254
276
|
this.initializationStatus = 'ready';
|
|
@@ -271,30 +293,129 @@ export class MemoryLayerEngine {
|
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
/**
|
|
274
|
-
*
|
|
296
|
+
* Register idle-time maintenance tasks
|
|
275
297
|
*/
|
|
276
|
-
private
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Refresh git changes every 5 minutes
|
|
283
|
-
this.backgroundInterval = setInterval(() => {
|
|
284
|
-
try {
|
|
285
|
-
// Sync recent git changes
|
|
298
|
+
private registerIdleTasks(): void {
|
|
299
|
+
// Git sync when idle and HEAD has changed
|
|
300
|
+
this.activityGate.registerIdleTask('git-sync', () => {
|
|
301
|
+
if (this.gitStalenessChecker.hasNewCommits()) {
|
|
286
302
|
const synced = this.changeIntelligence.syncFromGit(20);
|
|
287
303
|
if (synced > 0) {
|
|
288
|
-
|
|
304
|
+
this.changeIntelligence.scanForBugFixes();
|
|
305
|
+
console.error(`Idle sync: ${synced} git changes synced`);
|
|
289
306
|
}
|
|
307
|
+
}
|
|
308
|
+
}, {
|
|
309
|
+
minIdleMs: 30_000, // 30 seconds idle
|
|
310
|
+
intervalMs: 60_000 // Check at most every minute
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Importance score updates when idle for longer
|
|
314
|
+
this.activityGate.registerIdleTask('importance-update', () => {
|
|
315
|
+
this.learningEngine.updateImportanceScores();
|
|
316
|
+
}, {
|
|
317
|
+
minIdleMs: 60_000, // 1 minute idle
|
|
318
|
+
intervalMs: 300_000 // Max once per 5 minutes
|
|
319
|
+
});
|
|
320
|
+
}
|
|
290
321
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Sync git changes on-demand with cheap pre-check
|
|
324
|
+
* Only syncs if HEAD has changed, otherwise returns 0
|
|
325
|
+
*/
|
|
326
|
+
syncGitChanges(limit: number = 20): number {
|
|
327
|
+
// Record activity
|
|
328
|
+
this.activityGate.recordActivity();
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
// Cheap pre-check: has HEAD changed?
|
|
332
|
+
if (!this.gitStalenessChecker.hasNewCommits()) {
|
|
333
|
+
return 0; // No new commits, skip expensive sync
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const synced = this.changeIntelligence.syncFromGit(limit);
|
|
337
|
+
if (synced > 0) {
|
|
338
|
+
// Also scan for bug fixes when syncing
|
|
339
|
+
this.changeIntelligence.scanForBugFixes();
|
|
340
|
+
}
|
|
341
|
+
return synced;
|
|
342
|
+
} catch {
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Force sync git changes, bypassing the staleness check
|
|
349
|
+
*/
|
|
350
|
+
forceSyncGitChanges(limit: number = 20): number {
|
|
351
|
+
this.activityGate.recordActivity();
|
|
352
|
+
this.gitStalenessChecker.updateCachedHead();
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const synced = this.changeIntelligence.syncFromGit(limit);
|
|
356
|
+
if (synced > 0) {
|
|
357
|
+
this.changeIntelligence.scanForBugFixes();
|
|
296
358
|
}
|
|
297
|
-
|
|
359
|
+
return synced;
|
|
360
|
+
} catch {
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Trigger a full refresh of the memory layer
|
|
367
|
+
* Syncs git changes, updates importance scores, etc.
|
|
368
|
+
*/
|
|
369
|
+
triggerRefresh(): {
|
|
370
|
+
gitSynced: number;
|
|
371
|
+
importanceUpdated: boolean;
|
|
372
|
+
tasksExecuted: string[];
|
|
373
|
+
} {
|
|
374
|
+
this.activityGate.recordActivity();
|
|
375
|
+
|
|
376
|
+
// Force git sync
|
|
377
|
+
this.gitStalenessChecker.updateCachedHead();
|
|
378
|
+
const gitSynced = this.forceSyncGitChanges();
|
|
379
|
+
|
|
380
|
+
// Update importance scores
|
|
381
|
+
this.learningEngine.updateImportanceScores();
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
gitSynced,
|
|
385
|
+
importanceUpdated: true,
|
|
386
|
+
tasksExecuted: ['git-sync', 'importance-update']
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get refresh system status
|
|
392
|
+
*/
|
|
393
|
+
getRefreshStatus(): {
|
|
394
|
+
lastActivity: number;
|
|
395
|
+
isIdle: boolean;
|
|
396
|
+
idleDuration: number;
|
|
397
|
+
gitHead: string | null;
|
|
398
|
+
hasNewCommits: boolean;
|
|
399
|
+
idleTasks: Array<{
|
|
400
|
+
name: string;
|
|
401
|
+
lastRun: number;
|
|
402
|
+
readyToRun: boolean;
|
|
403
|
+
}>;
|
|
404
|
+
} {
|
|
405
|
+
const status = this.activityGate.getStatus();
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
lastActivity: this.activityGate.getLastActivity(),
|
|
409
|
+
isIdle: status.isIdle,
|
|
410
|
+
idleDuration: status.idleDuration,
|
|
411
|
+
gitHead: this.gitStalenessChecker.getCachedHead(),
|
|
412
|
+
hasNewCommits: this.gitStalenessChecker.hasNewCommits(),
|
|
413
|
+
idleTasks: status.tasks.map(t => ({
|
|
414
|
+
name: t.name,
|
|
415
|
+
lastRun: t.lastRun,
|
|
416
|
+
readyToRun: t.readyToRun
|
|
417
|
+
}))
|
|
418
|
+
};
|
|
298
419
|
}
|
|
299
420
|
|
|
300
421
|
/**
|
|
@@ -313,6 +434,9 @@ export class MemoryLayerEngine {
|
|
|
313
434
|
}
|
|
314
435
|
|
|
315
436
|
async getContext(query: string, currentFile?: string, maxTokens?: number): Promise<AssembledContext> {
|
|
437
|
+
// Record activity for refresh system
|
|
438
|
+
this.activityGate.recordActivity();
|
|
439
|
+
|
|
316
440
|
// Track the query
|
|
317
441
|
this.learningEngine.trackEvent({ eventType: 'query', query });
|
|
318
442
|
|
|
@@ -339,6 +463,7 @@ export class MemoryLayerEngine {
|
|
|
339
463
|
}
|
|
340
464
|
|
|
341
465
|
async searchCodebase(query: string, limit: number = 10): Promise<SearchResult[]> {
|
|
466
|
+
this.activityGate.recordActivity();
|
|
342
467
|
const embedding = await this.indexer.getEmbeddingGenerator().embed(query);
|
|
343
468
|
let results = this.tier2.search(embedding, limit * 2); // Get more for re-ranking
|
|
344
469
|
|
|
@@ -354,7 +479,28 @@ export class MemoryLayerEngine {
|
|
|
354
479
|
files?: string[],
|
|
355
480
|
tags?: string[]
|
|
356
481
|
): Promise<Decision> {
|
|
357
|
-
|
|
482
|
+
this.activityGate.recordActivity();
|
|
483
|
+
const decision = await this.decisionTracker.recordDecision(title, description, files || [], tags || []);
|
|
484
|
+
|
|
485
|
+
// Log activity for decision recording
|
|
486
|
+
this.livingDocs.getActivityTracker().logActivity(
|
|
487
|
+
'decision_recorded',
|
|
488
|
+
`Decision: ${title}`,
|
|
489
|
+
undefined,
|
|
490
|
+
{ decisionId: decision.id }
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// Auto-mark decisions as critical context
|
|
494
|
+
this.contextRotPrevention.markCritical(
|
|
495
|
+
`Decision: ${title}\n${description}`,
|
|
496
|
+
{
|
|
497
|
+
type: 'decision',
|
|
498
|
+
reason: 'Architectural decision',
|
|
499
|
+
source: 'auto'
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
return decision;
|
|
358
504
|
}
|
|
359
505
|
|
|
360
506
|
getRecentDecisions(limit: number = 10): Decision[] {
|
|
@@ -370,6 +516,7 @@ export class MemoryLayerEngine {
|
|
|
370
516
|
}
|
|
371
517
|
|
|
372
518
|
async getFileContext(filePath: string): Promise<{ content: string; language: string; lines: number } | null> {
|
|
519
|
+
this.activityGate.recordActivity();
|
|
373
520
|
const absolutePath = join(this.config.projectPath, filePath);
|
|
374
521
|
|
|
375
522
|
if (!existsSync(absolutePath)) {
|
|
@@ -1440,14 +1587,8 @@ export class MemoryLayerEngine {
|
|
|
1440
1587
|
|
|
1441
1588
|
shutdown(): void {
|
|
1442
1589
|
console.error('Shutting down MemoryLayer...');
|
|
1443
|
-
|
|
1444
|
-
// Stop background intelligence
|
|
1445
|
-
if (this.backgroundInterval) {
|
|
1446
|
-
clearInterval(this.backgroundInterval);
|
|
1447
|
-
this.backgroundInterval = null;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
1590
|
this.indexer.stopWatching();
|
|
1591
|
+
this.activityGate.shutdown();
|
|
1451
1592
|
this.tier1.save();
|
|
1452
1593
|
this.featureContextManager.shutdown();
|
|
1453
1594
|
closeDatabase(this.db);
|
package/src/core/learning.ts
CHANGED
|
@@ -37,6 +37,8 @@ export class LearningEngine {
|
|
|
37
37
|
private hotCache: Map<string, { content: string; accessCount: number; lastAccessed: number }> = new Map();
|
|
38
38
|
private readonly MAX_CACHE_SIZE = 50;
|
|
39
39
|
private readonly CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
40
|
+
private lastImportanceUpdate: number = 0;
|
|
41
|
+
private readonly IMPORTANCE_UPDATE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
40
42
|
|
|
41
43
|
constructor(db: Database.Database) {
|
|
42
44
|
this.db = db;
|
|
@@ -385,11 +387,40 @@ export class LearningEngine {
|
|
|
385
387
|
return result.changes;
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Check if importance update should be skipped
|
|
392
|
+
* Returns true if we've already updated within the last 5 minutes
|
|
393
|
+
*/
|
|
394
|
+
shouldSkipImportanceUpdate(): boolean {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
return now - this.lastImportanceUpdate < this.IMPORTANCE_UPDATE_INTERVAL_MS;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get time since last importance update in milliseconds
|
|
401
|
+
*/
|
|
402
|
+
getTimeSinceLastImportanceUpdate(): number {
|
|
403
|
+
return Date.now() - this.lastImportanceUpdate;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get the last importance update timestamp
|
|
408
|
+
*/
|
|
409
|
+
getLastImportanceUpdateTime(): number {
|
|
410
|
+
return this.lastImportanceUpdate;
|
|
411
|
+
}
|
|
412
|
+
|
|
388
413
|
/**
|
|
389
414
|
* Update importance scores for all files based on usage patterns
|
|
390
415
|
* Called by background intelligence loop
|
|
416
|
+
* Gated to run at most once per 5 minutes
|
|
391
417
|
*/
|
|
392
418
|
updateImportanceScores(): void {
|
|
419
|
+
// Gate: skip if we've already updated recently
|
|
420
|
+
if (this.shouldSkipImportanceUpdate()) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
393
424
|
try {
|
|
394
425
|
// Get all files with access stats
|
|
395
426
|
const stmt = this.db.prepare(`
|
|
@@ -428,6 +459,9 @@ export class LearningEngine {
|
|
|
428
459
|
importance
|
|
429
460
|
);
|
|
430
461
|
}
|
|
462
|
+
|
|
463
|
+
// Update the timestamp to prevent running again within the interval
|
|
464
|
+
this.lastImportanceUpdate = Date.now();
|
|
431
465
|
} catch (error) {
|
|
432
466
|
console.error('Error updating importance scores:', error);
|
|
433
467
|
}
|
|
@@ -40,6 +40,9 @@ export class LivingDocumentationEngine {
|
|
|
40
40
|
async generateArchitectureDocs(): Promise<ArchitectureDoc> {
|
|
41
41
|
const doc = await this.archGen.generate();
|
|
42
42
|
|
|
43
|
+
// Store architecture docs in documentation table
|
|
44
|
+
this.storeDocumentation('_architecture', 'architecture', JSON.stringify(doc));
|
|
45
|
+
|
|
43
46
|
// Log activity
|
|
44
47
|
this.activityTracker.logActivity(
|
|
45
48
|
'doc_generation',
|
|
@@ -51,6 +54,13 @@ export class LivingDocumentationEngine {
|
|
|
51
54
|
return doc;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Get the activity tracker for external use
|
|
59
|
+
*/
|
|
60
|
+
getActivityTracker(): ActivityTracker {
|
|
61
|
+
return this.activityTracker;
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
async generateComponentDoc(filePath: string): Promise<ComponentDoc> {
|
|
55
65
|
const doc = await this.compGen.generate(filePath);
|
|
56
66
|
|
|
@@ -89,6 +99,20 @@ export class LivingDocumentationEngine {
|
|
|
89
99
|
|
|
90
100
|
private storeDocumentation(filePath: string, docType: string, content: string): void {
|
|
91
101
|
try {
|
|
102
|
+
// Special handling for architecture docs (no file ID)
|
|
103
|
+
if (filePath === '_architecture') {
|
|
104
|
+
// Use file_id = 0 for special docs like architecture
|
|
105
|
+
const stmt = this.db.prepare(`
|
|
106
|
+
INSERT INTO documentation (file_id, doc_type, content, generated_at)
|
|
107
|
+
VALUES (0, ?, ?, unixepoch())
|
|
108
|
+
ON CONFLICT(file_id, doc_type) DO UPDATE SET
|
|
109
|
+
content = excluded.content,
|
|
110
|
+
generated_at = unixepoch()
|
|
111
|
+
`);
|
|
112
|
+
stmt.run(docType, content);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
// Get file ID
|
|
93
117
|
const fileStmt = this.db.prepare('SELECT id FROM files WHERE path = ?');
|
|
94
118
|
const fileRow = fileStmt.get(filePath) as { id: number } | undefined;
|