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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ghost Mode - Silent Intelligence Layer
|
|
3
|
+
*
|
|
4
|
+
* Silently tracks what files Claude reads/writes. When code is written that
|
|
5
|
+
* touches a file with recorded decisions, automatically checks for conflicts.
|
|
6
|
+
* Makes MemoryLayer feel "telepathic" by surfacing relevant context proactively.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Decision } from '../types/index.js';
|
|
10
|
+
import type { Tier2Storage } from '../storage/tier2.js';
|
|
11
|
+
import type { EmbeddingGenerator } from '../indexing/embeddings.js';
|
|
12
|
+
|
|
13
|
+
export interface ConflictWarning {
|
|
14
|
+
decision: Decision;
|
|
15
|
+
warning: string;
|
|
16
|
+
severity: 'low' | 'medium' | 'high';
|
|
17
|
+
matchedTerms: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FileContext {
|
|
21
|
+
path: string;
|
|
22
|
+
accessedAt: Date;
|
|
23
|
+
relatedDecisions: Decision[];
|
|
24
|
+
relatedPatterns: string[];
|
|
25
|
+
accessCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GhostInsight {
|
|
29
|
+
activeFiles: string[];
|
|
30
|
+
recentDecisions: Decision[];
|
|
31
|
+
potentialConflicts: ConflictWarning[];
|
|
32
|
+
suggestions: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Decision keywords that indicate strong stances
|
|
36
|
+
const DECISION_INDICATORS = [
|
|
37
|
+
'use', 'always', 'never', 'prefer', 'avoid', 'must', 'should',
|
|
38
|
+
'instead of', 'rather than', 'not', 'don\'t', 'do not',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Technology/pattern keywords for matching
|
|
42
|
+
const TECH_PATTERNS = [
|
|
43
|
+
// Auth
|
|
44
|
+
{ pattern: /\b(jwt|json\s*web\s*token)\b/i, category: 'auth', term: 'JWT' },
|
|
45
|
+
{ pattern: /\b(session|cookie)\b/i, category: 'auth', term: 'session' },
|
|
46
|
+
{ pattern: /\b(oauth|o-?auth)\b/i, category: 'auth', term: 'OAuth' },
|
|
47
|
+
// Database
|
|
48
|
+
{ pattern: /\b(sql|mysql|postgres|postgresql)\b/i, category: 'database', term: 'SQL' },
|
|
49
|
+
{ pattern: /\b(mongo|mongodb|nosql)\b/i, category: 'database', term: 'MongoDB' },
|
|
50
|
+
{ pattern: /\b(redis|memcache)\b/i, category: 'cache', term: 'Redis' },
|
|
51
|
+
// State
|
|
52
|
+
{ pattern: /\b(redux|zustand|mobx)\b/i, category: 'state', term: 'state-management' },
|
|
53
|
+
{ pattern: /\b(context\s*api|useContext)\b/i, category: 'state', term: 'Context API' },
|
|
54
|
+
// Testing
|
|
55
|
+
{ pattern: /\b(jest|vitest|mocha)\b/i, category: 'testing', term: 'testing-framework' },
|
|
56
|
+
{ pattern: /\b(enzyme|testing-library|rtl)\b/i, category: 'testing', term: 'testing-library' },
|
|
57
|
+
// API
|
|
58
|
+
{ pattern: /\b(rest|restful)\b/i, category: 'api', term: 'REST' },
|
|
59
|
+
{ pattern: /\b(graphql|gql)\b/i, category: 'api', term: 'GraphQL' },
|
|
60
|
+
{ pattern: /\b(grpc|protobuf)\b/i, category: 'api', term: 'gRPC' },
|
|
61
|
+
// Style
|
|
62
|
+
{ pattern: /\b(tailwind|tailwindcss)\b/i, category: 'styling', term: 'Tailwind' },
|
|
63
|
+
{ pattern: /\b(styled-components|emotion)\b/i, category: 'styling', term: 'CSS-in-JS' },
|
|
64
|
+
{ pattern: /\b(sass|scss|less)\b/i, category: 'styling', term: 'CSS preprocessor' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export class GhostMode {
|
|
68
|
+
private activeFiles: Map<string, FileContext> = new Map();
|
|
69
|
+
private recentDecisions: Map<string, Decision[]> = new Map();
|
|
70
|
+
private tier2: Tier2Storage;
|
|
71
|
+
private embeddingGenerator: EmbeddingGenerator;
|
|
72
|
+
|
|
73
|
+
// Ghost mode settings
|
|
74
|
+
private readonly MAX_ACTIVE_FILES = 20;
|
|
75
|
+
private readonly FILE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
76
|
+
private readonly DECISION_CACHE_SIZE = 50;
|
|
77
|
+
|
|
78
|
+
constructor(tier2: Tier2Storage, embeddingGenerator: EmbeddingGenerator) {
|
|
79
|
+
this.tier2 = tier2;
|
|
80
|
+
this.embeddingGenerator = embeddingGenerator;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Called when any file is read - silently track and pre-fetch decisions
|
|
85
|
+
*/
|
|
86
|
+
async onFileAccess(filePath: string): Promise<void> {
|
|
87
|
+
const now = new Date();
|
|
88
|
+
|
|
89
|
+
// Update or create file context
|
|
90
|
+
const existing = this.activeFiles.get(filePath);
|
|
91
|
+
if (existing) {
|
|
92
|
+
existing.accessedAt = now;
|
|
93
|
+
existing.accessCount++;
|
|
94
|
+
} else {
|
|
95
|
+
// Pre-fetch related decisions for this file
|
|
96
|
+
const relatedDecisions = await this.findRelatedDecisions(filePath);
|
|
97
|
+
const relatedPatterns = await this.findRelatedPatterns(filePath);
|
|
98
|
+
|
|
99
|
+
this.activeFiles.set(filePath, {
|
|
100
|
+
path: filePath,
|
|
101
|
+
accessedAt: now,
|
|
102
|
+
relatedDecisions,
|
|
103
|
+
relatedPatterns,
|
|
104
|
+
accessCount: 1,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Cache decisions for quick conflict checking
|
|
108
|
+
this.recentDecisions.set(filePath, relatedDecisions);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Evict stale entries
|
|
112
|
+
this.evictStaleFiles();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Called before code is written - returns potential conflicts
|
|
117
|
+
*/
|
|
118
|
+
checkConflicts(code: string, targetFile?: string): ConflictWarning[] {
|
|
119
|
+
const warnings: ConflictWarning[] = [];
|
|
120
|
+
const codeTerms = this.extractTerms(code);
|
|
121
|
+
|
|
122
|
+
// Get decisions to check against
|
|
123
|
+
let decisionsToCheck: Decision[] = [];
|
|
124
|
+
|
|
125
|
+
if (targetFile && this.recentDecisions.has(targetFile)) {
|
|
126
|
+
decisionsToCheck = this.recentDecisions.get(targetFile) || [];
|
|
127
|
+
} else {
|
|
128
|
+
// Check against all cached decisions
|
|
129
|
+
for (const decisions of this.recentDecisions.values()) {
|
|
130
|
+
decisionsToCheck.push(...decisions);
|
|
131
|
+
}
|
|
132
|
+
// Deduplicate
|
|
133
|
+
decisionsToCheck = this.deduplicateDecisions(decisionsToCheck);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check each decision for conflicts
|
|
137
|
+
for (const decision of decisionsToCheck) {
|
|
138
|
+
const conflict = this.detectConflict(code, codeTerms, decision);
|
|
139
|
+
if (conflict) {
|
|
140
|
+
warnings.push(conflict);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Sort by severity
|
|
145
|
+
warnings.sort((a, b) => {
|
|
146
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
147
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return warnings;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get current ghost insight - what the system knows about current work
|
|
155
|
+
*/
|
|
156
|
+
getInsight(): GhostInsight {
|
|
157
|
+
const activeFiles = Array.from(this.activeFiles.keys());
|
|
158
|
+
const recentDecisions = this.getRecentUniqueDecisions();
|
|
159
|
+
const suggestions = this.generateSuggestions();
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
activeFiles,
|
|
163
|
+
recentDecisions,
|
|
164
|
+
potentialConflicts: [],
|
|
165
|
+
suggestions,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get ghost insight with conflict check for specific code
|
|
171
|
+
*/
|
|
172
|
+
getInsightForCode(code: string, targetFile?: string): GhostInsight {
|
|
173
|
+
const insight = this.getInsight();
|
|
174
|
+
insight.potentialConflicts = this.checkConflicts(code, targetFile);
|
|
175
|
+
return insight;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clear ghost mode state
|
|
180
|
+
*/
|
|
181
|
+
clear(): void {
|
|
182
|
+
this.activeFiles.clear();
|
|
183
|
+
this.recentDecisions.clear();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get files most recently accessed
|
|
188
|
+
*/
|
|
189
|
+
getRecentFiles(limit: number = 10): string[] {
|
|
190
|
+
return Array.from(this.activeFiles.entries())
|
|
191
|
+
.sort((a, b) => b[1].accessedAt.getTime() - a[1].accessedAt.getTime())
|
|
192
|
+
.slice(0, limit)
|
|
193
|
+
.map(([path]) => path);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get decisions related to recently accessed files
|
|
198
|
+
*/
|
|
199
|
+
getRecentUniqueDecisions(limit: number = 10): Decision[] {
|
|
200
|
+
const allDecisions: Decision[] = [];
|
|
201
|
+
for (const decisions of this.recentDecisions.values()) {
|
|
202
|
+
allDecisions.push(...decisions);
|
|
203
|
+
}
|
|
204
|
+
return this.deduplicateDecisions(allDecisions).slice(0, limit);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ========== Private Methods ==========
|
|
208
|
+
|
|
209
|
+
private async findRelatedDecisions(filePath: string): Promise<Decision[]> {
|
|
210
|
+
try {
|
|
211
|
+
// Search decisions by file path
|
|
212
|
+
const pathParts = filePath.split(/[/\\]/);
|
|
213
|
+
const fileName = pathParts[pathParts.length - 1] || '';
|
|
214
|
+
const dirName = pathParts[pathParts.length - 2] || '';
|
|
215
|
+
|
|
216
|
+
// Create a search query from file context
|
|
217
|
+
const searchQuery = `${dirName} ${fileName.replace(/\.[^.]+$/, '')}`;
|
|
218
|
+
const embedding = await this.embeddingGenerator.embed(searchQuery);
|
|
219
|
+
|
|
220
|
+
return this.tier2.searchDecisions(embedding, 5);
|
|
221
|
+
} catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private findRelatedPatterns(filePath: string): string[] {
|
|
227
|
+
// Extract patterns from file extension and path
|
|
228
|
+
const patterns: string[] = [];
|
|
229
|
+
|
|
230
|
+
if (filePath.includes('test') || filePath.includes('spec')) {
|
|
231
|
+
patterns.push('testing');
|
|
232
|
+
}
|
|
233
|
+
if (filePath.includes('api') || filePath.includes('route')) {
|
|
234
|
+
patterns.push('api');
|
|
235
|
+
}
|
|
236
|
+
if (filePath.includes('component') || filePath.includes('ui')) {
|
|
237
|
+
patterns.push('ui');
|
|
238
|
+
}
|
|
239
|
+
if (filePath.includes('model') || filePath.includes('schema')) {
|
|
240
|
+
patterns.push('data-model');
|
|
241
|
+
}
|
|
242
|
+
if (filePath.includes('auth')) {
|
|
243
|
+
patterns.push('authentication');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return patterns;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private extractTerms(text: string): Set<string> {
|
|
250
|
+
const terms = new Set<string>();
|
|
251
|
+
|
|
252
|
+
// Extract technology/pattern terms
|
|
253
|
+
for (const { pattern, term } of TECH_PATTERNS) {
|
|
254
|
+
if (pattern.test(text)) {
|
|
255
|
+
terms.add(term.toLowerCase());
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Extract common programming terms
|
|
260
|
+
const words = text.toLowerCase().match(/\b[a-z]+\b/g) || [];
|
|
261
|
+
for (const word of words) {
|
|
262
|
+
if (word.length > 3) {
|
|
263
|
+
terms.add(word);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return terms;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private detectConflict(
|
|
271
|
+
code: string,
|
|
272
|
+
codeTerms: Set<string>,
|
|
273
|
+
decision: Decision
|
|
274
|
+
): ConflictWarning | null {
|
|
275
|
+
const decisionText = `${decision.title} ${decision.description}`.toLowerCase();
|
|
276
|
+
const decisionTerms = this.extractTerms(decisionText);
|
|
277
|
+
|
|
278
|
+
// Check for technology conflicts
|
|
279
|
+
const matchedTerms: string[] = [];
|
|
280
|
+
for (const term of codeTerms) {
|
|
281
|
+
if (decisionTerms.has(term)) {
|
|
282
|
+
matchedTerms.push(term);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (matchedTerms.length === 0) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check if decision opposes this technology
|
|
291
|
+
const negativePattern = new RegExp(
|
|
292
|
+
`(don't|do not|never|avoid|not)\\s+.{0,30}\\b(${matchedTerms.join('|')})\\b`,
|
|
293
|
+
'i'
|
|
294
|
+
);
|
|
295
|
+
const preferOtherPattern = new RegExp(
|
|
296
|
+
`(instead of|rather than)\\s+.{0,30}\\b(${matchedTerms.join('|')})\\b`,
|
|
297
|
+
'i'
|
|
298
|
+
);
|
|
299
|
+
const isNegative = negativePattern.test(decisionText) || preferOtherPattern.test(decisionText);
|
|
300
|
+
|
|
301
|
+
if (!isNegative) {
|
|
302
|
+
// Check if decision uses a different technology in the same category
|
|
303
|
+
for (const { pattern, category, term } of TECH_PATTERNS) {
|
|
304
|
+
if (matchedTerms.some(m => m.toLowerCase() === term.toLowerCase())) {
|
|
305
|
+
continue; // Skip the term we're using
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (pattern.test(decisionText)) {
|
|
309
|
+
// Decision mentions a different tech in same category
|
|
310
|
+
const codeUsesCategory = matchedTerms.some(m => {
|
|
311
|
+
const match = TECH_PATTERNS.find(p => p.term.toLowerCase() === m.toLowerCase());
|
|
312
|
+
return match && match.category === category;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (codeUsesCategory) {
|
|
316
|
+
return {
|
|
317
|
+
decision,
|
|
318
|
+
warning: `This code uses ${matchedTerms.join(', ')} but decision "${decision.title}" suggests using ${term}`,
|
|
319
|
+
severity: 'medium',
|
|
320
|
+
matchedTerms,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Determine severity
|
|
330
|
+
let severity: 'low' | 'medium' | 'high' = 'low';
|
|
331
|
+
if (decisionText.includes('must') || decisionText.includes('never') || decisionText.includes('always')) {
|
|
332
|
+
severity = 'high';
|
|
333
|
+
} else if (decisionText.includes('should') || decisionText.includes('prefer')) {
|
|
334
|
+
severity = 'medium';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
decision,
|
|
339
|
+
warning: `This code may conflict with decision: "${decision.title}"`,
|
|
340
|
+
severity,
|
|
341
|
+
matchedTerms,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private generateSuggestions(): string[] {
|
|
346
|
+
const suggestions: string[] = [];
|
|
347
|
+
|
|
348
|
+
// Suggest based on active files
|
|
349
|
+
const recentFiles = this.getRecentFiles(5);
|
|
350
|
+
if (recentFiles.length > 0) {
|
|
351
|
+
const categories = new Set<string>();
|
|
352
|
+
for (const file of recentFiles) {
|
|
353
|
+
const ctx = this.activeFiles.get(file);
|
|
354
|
+
if (ctx) {
|
|
355
|
+
ctx.relatedPatterns.forEach(p => categories.add(p));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (categories.size > 0) {
|
|
360
|
+
suggestions.push(`Working on: ${Array.from(categories).join(', ')}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Suggest based on decisions
|
|
365
|
+
const decisions = this.getRecentUniqueDecisions(3);
|
|
366
|
+
if (decisions.length > 0) {
|
|
367
|
+
suggestions.push(`Relevant decisions: ${decisions.map(d => d.title).join(', ')}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return suggestions;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private deduplicateDecisions(decisions: Decision[]): Decision[] {
|
|
374
|
+
const seen = new Set<string>();
|
|
375
|
+
return decisions.filter(d => {
|
|
376
|
+
if (seen.has(d.id)) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
seen.add(d.id);
|
|
380
|
+
return true;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private evictStaleFiles(): void {
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
const toEvict: string[] = [];
|
|
387
|
+
|
|
388
|
+
for (const [path, context] of this.activeFiles.entries()) {
|
|
389
|
+
if (now - context.accessedAt.getTime() > this.FILE_TTL_MS) {
|
|
390
|
+
toEvict.push(path);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Evict stale files
|
|
395
|
+
for (const path of toEvict) {
|
|
396
|
+
this.activeFiles.delete(path);
|
|
397
|
+
this.recentDecisions.delete(path);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// If still too many, evict oldest
|
|
401
|
+
if (this.activeFiles.size > this.MAX_ACTIVE_FILES) {
|
|
402
|
+
const entries = Array.from(this.activeFiles.entries())
|
|
403
|
+
.sort((a, b) => a[1].accessedAt.getTime() - b[1].accessedAt.getTime());
|
|
404
|
+
|
|
405
|
+
const toRemove = entries.slice(0, entries.length - this.MAX_ACTIVE_FILES);
|
|
406
|
+
for (const [path] of toRemove) {
|
|
407
|
+
this.activeFiles.delete(path);
|
|
408
|
+
this.recentDecisions.delete(path);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|