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,282 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type Database from 'better-sqlite3';
|
|
4
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
5
|
+
import type { CodeSymbol } from '../../types/index.js';
|
|
6
|
+
import type {
|
|
7
|
+
ValidationResult,
|
|
8
|
+
OutdatedDoc,
|
|
9
|
+
UndocumentedItem,
|
|
10
|
+
DocSuggestion
|
|
11
|
+
} from '../../types/documentation.js';
|
|
12
|
+
|
|
13
|
+
export class DocValidator {
|
|
14
|
+
private projectPath: string;
|
|
15
|
+
private tier2: Tier2Storage;
|
|
16
|
+
private db: Database.Database;
|
|
17
|
+
|
|
18
|
+
constructor(projectPath: string, tier2: Tier2Storage, db: Database.Database) {
|
|
19
|
+
this.projectPath = projectPath;
|
|
20
|
+
this.tier2 = tier2;
|
|
21
|
+
this.db = db;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async validate(): Promise<ValidationResult> {
|
|
25
|
+
const outdated = await this.findOutdated();
|
|
26
|
+
const undocumented = await this.findUndocumented();
|
|
27
|
+
const suggestions = this.generateSuggestions(outdated, undocumented);
|
|
28
|
+
const score = this.calculateScore(outdated, undocumented);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
isValid: score >= 70,
|
|
32
|
+
outdatedDocs: outdated,
|
|
33
|
+
undocumentedCode: undocumented,
|
|
34
|
+
suggestions,
|
|
35
|
+
score
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async findOutdated(): Promise<OutdatedDoc[]> {
|
|
40
|
+
const outdated: OutdatedDoc[] = [];
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Get all documentation records
|
|
44
|
+
const stmt = this.db.prepare(`
|
|
45
|
+
SELECT d.file_id, d.generated_at, f.path, f.last_modified
|
|
46
|
+
FROM documentation d
|
|
47
|
+
JOIN files f ON d.file_id = f.id
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
const rows = stmt.all() as Array<{
|
|
51
|
+
file_id: number;
|
|
52
|
+
generated_at: number;
|
|
53
|
+
path: string;
|
|
54
|
+
last_modified: number;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
// Check if code changed after docs were generated
|
|
59
|
+
if (row.last_modified > row.generated_at) {
|
|
60
|
+
const daysSinceUpdate = Math.floor(
|
|
61
|
+
(Date.now() / 1000 - row.generated_at) / (24 * 60 * 60)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
outdated.push({
|
|
65
|
+
file: row.path,
|
|
66
|
+
reason: 'Code modified after documentation was generated',
|
|
67
|
+
lastDocUpdate: new Date(row.generated_at * 1000),
|
|
68
|
+
lastCodeChange: new Date(row.last_modified * 1000),
|
|
69
|
+
severity: daysSinceUpdate > 30 ? 'high' : daysSinceUpdate > 7 ? 'medium' : 'low'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Documentation table might not exist yet
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return outdated;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async findUndocumented(options?: {
|
|
81
|
+
importance?: 'low' | 'medium' | 'high' | 'all';
|
|
82
|
+
type?: 'file' | 'function' | 'class' | 'interface' | 'all';
|
|
83
|
+
}): Promise<UndocumentedItem[]> {
|
|
84
|
+
const items: UndocumentedItem[] = [];
|
|
85
|
+
const files = this.tier2.getAllFiles();
|
|
86
|
+
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const symbols = this.tier2.getSymbolsByFile(file.id);
|
|
89
|
+
|
|
90
|
+
// Check for undocumented exported symbols
|
|
91
|
+
const exportedSymbols = symbols.filter(s => s.exported);
|
|
92
|
+
|
|
93
|
+
for (const symbol of exportedSymbols) {
|
|
94
|
+
if (!symbol.docstring || symbol.docstring.trim().length === 0) {
|
|
95
|
+
const importance = this.calculateImportance(file.path, symbol);
|
|
96
|
+
|
|
97
|
+
// Apply filters
|
|
98
|
+
if (options?.importance && options.importance !== 'all' && importance !== options.importance) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const symbolType = this.mapSymbolKindToType(symbol.kind);
|
|
103
|
+
if (options?.type && options.type !== 'all' && symbolType !== options.type) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
items.push({
|
|
108
|
+
file: file.path,
|
|
109
|
+
symbol: symbol.name,
|
|
110
|
+
type: symbolType,
|
|
111
|
+
importance
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for files without any documentation
|
|
117
|
+
if (exportedSymbols.length === 0 && symbols.length > 0) {
|
|
118
|
+
// File has symbols but none exported - might be internal
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if the file has any JSDoc/docstring at file level
|
|
123
|
+
const hasFileDoc = symbols.some(s => s.docstring && s.lineStart <= 5);
|
|
124
|
+
if (!hasFileDoc && exportedSymbols.length > 0) {
|
|
125
|
+
const importance = this.calculateFileImportance(file.path);
|
|
126
|
+
|
|
127
|
+
if (options?.importance && options.importance !== 'all' && importance !== options.importance) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (options?.type && options.type !== 'all' && options.type !== 'file') {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Only add if the file isn't already represented by undocumented symbols
|
|
135
|
+
const hasSymbolEntry = items.some(i => i.file === file.path);
|
|
136
|
+
if (!hasSymbolEntry) {
|
|
137
|
+
items.push({
|
|
138
|
+
file: file.path,
|
|
139
|
+
type: 'file',
|
|
140
|
+
importance
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sort by importance
|
|
147
|
+
const importanceOrder = { high: 0, medium: 1, low: 2 };
|
|
148
|
+
items.sort((a, b) => importanceOrder[a.importance] - importanceOrder[b.importance]);
|
|
149
|
+
|
|
150
|
+
return items;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private calculateImportance(filePath: string, symbol: CodeSymbol): 'low' | 'medium' | 'high' {
|
|
154
|
+
// Check how many files depend on this file
|
|
155
|
+
const dependents = this.tier2.getFileDependents(filePath);
|
|
156
|
+
|
|
157
|
+
// High importance: many dependents, or it's a class/interface
|
|
158
|
+
if (dependents.length >= 3 || symbol.kind === 'class' || symbol.kind === 'interface') {
|
|
159
|
+
return 'high';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Medium importance: some dependents, or it's a function
|
|
163
|
+
if (dependents.length >= 1 || symbol.kind === 'function') {
|
|
164
|
+
return 'medium';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 'low';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private calculateFileImportance(filePath: string): 'low' | 'medium' | 'high' {
|
|
171
|
+
const dependents = this.tier2.getFileDependents(filePath);
|
|
172
|
+
|
|
173
|
+
// Check if it's an index/entry file
|
|
174
|
+
if (filePath.includes('index.') || filePath.includes('/src/')) {
|
|
175
|
+
return 'high';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (dependents.length >= 5) return 'high';
|
|
179
|
+
if (dependents.length >= 2) return 'medium';
|
|
180
|
+
return 'low';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private mapSymbolKindToType(kind: string): 'file' | 'function' | 'class' | 'interface' {
|
|
184
|
+
switch (kind) {
|
|
185
|
+
case 'class':
|
|
186
|
+
return 'class';
|
|
187
|
+
case 'interface':
|
|
188
|
+
case 'type':
|
|
189
|
+
return 'interface';
|
|
190
|
+
case 'function':
|
|
191
|
+
case 'method':
|
|
192
|
+
return 'function';
|
|
193
|
+
default:
|
|
194
|
+
return 'function';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private generateSuggestions(
|
|
199
|
+
outdated: OutdatedDoc[],
|
|
200
|
+
undocumented: UndocumentedItem[]
|
|
201
|
+
): DocSuggestion[] {
|
|
202
|
+
const suggestions: DocSuggestion[] = [];
|
|
203
|
+
|
|
204
|
+
// Suggestions for outdated docs
|
|
205
|
+
for (const doc of outdated) {
|
|
206
|
+
suggestions.push({
|
|
207
|
+
file: doc.file,
|
|
208
|
+
suggestion: `Update documentation - code changed ${this.formatTimeDiff(doc.lastCodeChange, doc.lastDocUpdate)} after docs`,
|
|
209
|
+
priority: doc.severity
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Suggestions for undocumented code
|
|
214
|
+
const highPriorityUndoc = undocumented.filter(u => u.importance === 'high');
|
|
215
|
+
for (const item of highPriorityUndoc) {
|
|
216
|
+
suggestions.push({
|
|
217
|
+
file: item.file,
|
|
218
|
+
suggestion: item.symbol
|
|
219
|
+
? `Add documentation for exported ${item.type} '${item.symbol}'`
|
|
220
|
+
: `Add file-level documentation`,
|
|
221
|
+
priority: 'high'
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Group medium priority suggestions
|
|
226
|
+
const mediumPriorityCount = undocumented.filter(u => u.importance === 'medium').length;
|
|
227
|
+
if (mediumPriorityCount > 0) {
|
|
228
|
+
const files = [...new Set(undocumented.filter(u => u.importance === 'medium').map(u => u.file))];
|
|
229
|
+
if (files.length <= 3) {
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
suggestions.push({
|
|
232
|
+
file,
|
|
233
|
+
suggestion: 'Add documentation for exported symbols',
|
|
234
|
+
priority: 'medium'
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
suggestions.push({
|
|
239
|
+
file: files[0]!,
|
|
240
|
+
suggestion: `${mediumPriorityCount} symbols across ${files.length} files need documentation`,
|
|
241
|
+
priority: 'medium'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return suggestions;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private formatTimeDiff(later: Date, earlier: Date): string {
|
|
250
|
+
const diffMs = later.getTime() - earlier.getTime();
|
|
251
|
+
const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
|
|
252
|
+
|
|
253
|
+
if (diffDays === 0) return 'today';
|
|
254
|
+
if (diffDays === 1) return '1 day';
|
|
255
|
+
if (diffDays < 7) return `${diffDays} days`;
|
|
256
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks`;
|
|
257
|
+
return `${Math.floor(diffDays / 30)} months`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private calculateScore(outdated: OutdatedDoc[], undocumented: UndocumentedItem[]): number {
|
|
261
|
+
const totalFiles = this.tier2.getFileCount();
|
|
262
|
+
if (totalFiles === 0) return 100;
|
|
263
|
+
|
|
264
|
+
// Calculate based on:
|
|
265
|
+
// - % of files with outdated docs (30% weight)
|
|
266
|
+
// - % of high-importance items undocumented (50% weight)
|
|
267
|
+
// - % of medium-importance items undocumented (20% weight)
|
|
268
|
+
|
|
269
|
+
const outdatedPenalty = (outdated.length / totalFiles) * 30;
|
|
270
|
+
|
|
271
|
+
const highUndoc = undocumented.filter(u => u.importance === 'high').length;
|
|
272
|
+
const mediumUndoc = undocumented.filter(u => u.importance === 'medium').length;
|
|
273
|
+
|
|
274
|
+
// Assume each file has ~3 documentable items on average
|
|
275
|
+
const estimatedTotalItems = totalFiles * 3;
|
|
276
|
+
const highPenalty = estimatedTotalItems > 0 ? (highUndoc / estimatedTotalItems) * 50 : 0;
|
|
277
|
+
const mediumPenalty = estimatedTotalItems > 0 ? (mediumUndoc / estimatedTotalItems) * 20 : 0;
|
|
278
|
+
|
|
279
|
+
const totalPenalty = Math.min(100, outdatedPenalty + highPenalty + mediumPenalty);
|
|
280
|
+
return Math.round(100 - totalPenalty);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Living Documentation Module - Barrel Export
|
|
2
|
+
|
|
3
|
+
export { LivingDocumentationEngine } from './doc-engine.js';
|
|
4
|
+
export { ArchitectureGenerator } from './architecture-generator.js';
|
|
5
|
+
export { ComponentGenerator } from './component-generator.js';
|
|
6
|
+
export { ChangelogGenerator } from './changelog-generator.js';
|
|
7
|
+
export { DocValidator } from './doc-validator.js';
|
|
8
|
+
export { ActivityTracker } from './activity-tracker.js';
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, basename, resolve } from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import type { MemoryLayerConfig } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
export interface ProjectInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
dataDir: string;
|
|
13
|
+
lastAccessed: number;
|
|
14
|
+
totalFiles: number;
|
|
15
|
+
totalDecisions: number;
|
|
16
|
+
languages: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProjectRegistry {
|
|
20
|
+
version: number;
|
|
21
|
+
activeProject: string | null;
|
|
22
|
+
projects: Record<string, ProjectInfo>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ProjectManager {
|
|
26
|
+
private registryPath: string;
|
|
27
|
+
private registry: ProjectRegistry;
|
|
28
|
+
private baseDataDir: string;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.baseDataDir = join(homedir(), '.memorylayer');
|
|
32
|
+
this.registryPath = join(this.baseDataDir, 'registry.json');
|
|
33
|
+
|
|
34
|
+
// Ensure base directory exists
|
|
35
|
+
if (!existsSync(this.baseDataDir)) {
|
|
36
|
+
mkdirSync(this.baseDataDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.registry = this.loadRegistry();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private loadRegistry(): ProjectRegistry {
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(this.registryPath)) {
|
|
45
|
+
const data = JSON.parse(readFileSync(this.registryPath, 'utf-8'));
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error loading registry:', error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
version: 1,
|
|
54
|
+
activeProject: null,
|
|
55
|
+
projects: {}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private saveRegistry(): void {
|
|
60
|
+
try {
|
|
61
|
+
writeFileSync(this.registryPath, JSON.stringify(this.registry, null, 2));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error saving registry:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private generateProjectId(projectPath: string): string {
|
|
68
|
+
const normalizedPath = resolve(projectPath);
|
|
69
|
+
return createHash('md5').update(normalizedPath).digest('hex').slice(0, 12);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private getProjectDataDir(projectPath: string): string {
|
|
73
|
+
const projectId = this.generateProjectId(projectPath);
|
|
74
|
+
const projectName = basename(projectPath);
|
|
75
|
+
return join(this.baseDataDir, 'projects', `${projectName}-${projectId}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Register a new project or update existing
|
|
79
|
+
registerProject(projectPath: string): ProjectInfo {
|
|
80
|
+
const normalizedPath = resolve(projectPath);
|
|
81
|
+
const projectId = this.generateProjectId(normalizedPath);
|
|
82
|
+
const dataDir = this.getProjectDataDir(normalizedPath);
|
|
83
|
+
|
|
84
|
+
// Check if project directory exists
|
|
85
|
+
if (!existsSync(normalizedPath)) {
|
|
86
|
+
throw new Error(`Project path does not exist: ${normalizedPath}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const existingProject = this.registry.projects[projectId];
|
|
90
|
+
|
|
91
|
+
const projectInfo: ProjectInfo = {
|
|
92
|
+
id: projectId,
|
|
93
|
+
name: basename(normalizedPath),
|
|
94
|
+
path: normalizedPath,
|
|
95
|
+
dataDir,
|
|
96
|
+
lastAccessed: Date.now(),
|
|
97
|
+
totalFiles: existingProject?.totalFiles || 0,
|
|
98
|
+
totalDecisions: existingProject?.totalDecisions || 0,
|
|
99
|
+
languages: existingProject?.languages || []
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.registry.projects[projectId] = projectInfo;
|
|
103
|
+
this.saveRegistry();
|
|
104
|
+
|
|
105
|
+
return projectInfo;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Remove a project from registry
|
|
109
|
+
removeProject(projectId: string): boolean {
|
|
110
|
+
if (!this.registry.projects[projectId]) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
delete this.registry.projects[projectId];
|
|
115
|
+
|
|
116
|
+
if (this.registry.activeProject === projectId) {
|
|
117
|
+
this.registry.activeProject = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.saveRegistry();
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get all registered projects
|
|
125
|
+
listProjects(): ProjectInfo[] {
|
|
126
|
+
return Object.values(this.registry.projects)
|
|
127
|
+
.sort((a, b) => b.lastAccessed - a.lastAccessed);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get a specific project
|
|
131
|
+
getProject(projectId: string): ProjectInfo | null {
|
|
132
|
+
return this.registry.projects[projectId] || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Get project by path
|
|
136
|
+
getProjectByPath(projectPath: string): ProjectInfo | null {
|
|
137
|
+
const projectId = this.generateProjectId(projectPath);
|
|
138
|
+
return this.registry.projects[projectId] || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Set active project
|
|
142
|
+
setActiveProject(projectId: string): boolean {
|
|
143
|
+
if (!this.registry.projects[projectId]) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.registry.activeProject = projectId;
|
|
148
|
+
this.registry.projects[projectId]!.lastAccessed = Date.now();
|
|
149
|
+
this.saveRegistry();
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get active project
|
|
154
|
+
getActiveProject(): ProjectInfo | null {
|
|
155
|
+
if (!this.registry.activeProject) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return this.registry.projects[this.registry.activeProject] || null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Update project stats
|
|
162
|
+
updateProjectStats(projectId: string, stats: { totalFiles?: number; totalDecisions?: number; languages?: string[] }): void {
|
|
163
|
+
const project = this.registry.projects[projectId];
|
|
164
|
+
if (!project) return;
|
|
165
|
+
|
|
166
|
+
if (stats.totalFiles !== undefined) {
|
|
167
|
+
project.totalFiles = stats.totalFiles;
|
|
168
|
+
}
|
|
169
|
+
if (stats.totalDecisions !== undefined) {
|
|
170
|
+
project.totalDecisions = stats.totalDecisions;
|
|
171
|
+
}
|
|
172
|
+
if (stats.languages !== undefined) {
|
|
173
|
+
project.languages = stats.languages;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.saveRegistry();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Get config for a project
|
|
180
|
+
getProjectConfig(projectPath: string): MemoryLayerConfig {
|
|
181
|
+
const normalizedPath = resolve(projectPath);
|
|
182
|
+
const dataDir = this.getProjectDataDir(normalizedPath);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
projectPath: normalizedPath,
|
|
186
|
+
dataDir,
|
|
187
|
+
maxTokens: 6000,
|
|
188
|
+
embeddingModel: 'Xenova/all-MiniLM-L6-v2',
|
|
189
|
+
watchIgnore: [
|
|
190
|
+
'**/node_modules/**',
|
|
191
|
+
'**/.git/**',
|
|
192
|
+
'**/dist/**',
|
|
193
|
+
'**/build/**',
|
|
194
|
+
'**/.next/**',
|
|
195
|
+
'**/coverage/**',
|
|
196
|
+
'**/*.min.js',
|
|
197
|
+
'**/*.min.css',
|
|
198
|
+
'**/*.map',
|
|
199
|
+
'**/package-lock.json',
|
|
200
|
+
'**/yarn.lock',
|
|
201
|
+
'**/pnpm-lock.yaml',
|
|
202
|
+
'**/.env*',
|
|
203
|
+
'**/*.log'
|
|
204
|
+
]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Scan for projects in common locations
|
|
209
|
+
discoverProjects(): string[] {
|
|
210
|
+
const discovered: string[] = [];
|
|
211
|
+
const homeDir = homedir();
|
|
212
|
+
|
|
213
|
+
// Common project locations
|
|
214
|
+
const searchDirs = [
|
|
215
|
+
join(homeDir, 'projects'),
|
|
216
|
+
join(homeDir, 'Projects'),
|
|
217
|
+
join(homeDir, 'code'),
|
|
218
|
+
join(homeDir, 'Code'),
|
|
219
|
+
join(homeDir, 'dev'),
|
|
220
|
+
join(homeDir, 'Development'),
|
|
221
|
+
join(homeDir, 'workspace'),
|
|
222
|
+
join(homeDir, 'repos'),
|
|
223
|
+
join(homeDir, 'github'),
|
|
224
|
+
join(homeDir, 'Desktop'),
|
|
225
|
+
join(homeDir, 'Documents'),
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
for (const searchDir of searchDirs) {
|
|
229
|
+
if (!existsSync(searchDir)) continue;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const entries = readdirSync(searchDir, { withFileTypes: true });
|
|
233
|
+
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (!entry.isDirectory()) continue;
|
|
236
|
+
|
|
237
|
+
const projectPath = join(searchDir, entry.name);
|
|
238
|
+
|
|
239
|
+
// Check if it looks like a project (has common project files)
|
|
240
|
+
const projectIndicators = [
|
|
241
|
+
'package.json',
|
|
242
|
+
'Cargo.toml',
|
|
243
|
+
'go.mod',
|
|
244
|
+
'requirements.txt',
|
|
245
|
+
'pyproject.toml',
|
|
246
|
+
'pom.xml',
|
|
247
|
+
'build.gradle',
|
|
248
|
+
'.git'
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const isProject = projectIndicators.some(indicator =>
|
|
252
|
+
existsSync(join(projectPath, indicator))
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (isProject) {
|
|
256
|
+
discovered.push(projectPath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// Skip directories we can't read
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return discovered;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Get cross-project database connections for search
|
|
268
|
+
getProjectDatabases(): Array<{ project: ProjectInfo; db: Database.Database }> {
|
|
269
|
+
const result: Array<{ project: ProjectInfo; db: Database.Database }> = [];
|
|
270
|
+
|
|
271
|
+
for (const project of this.listProjects()) {
|
|
272
|
+
const dbPath = join(project.dataDir, 'memorylayer.db');
|
|
273
|
+
|
|
274
|
+
if (existsSync(dbPath)) {
|
|
275
|
+
try {
|
|
276
|
+
const db = new Database(dbPath, { readonly: true });
|
|
277
|
+
result.push({ project, db });
|
|
278
|
+
} catch {
|
|
279
|
+
// Skip databases we can't open
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Close all database connections
|
|
288
|
+
closeAllDatabases(dbs: Array<{ db: Database.Database }>): void {
|
|
289
|
+
for (const { db } of dbs) {
|
|
290
|
+
try {
|
|
291
|
+
db.close();
|
|
292
|
+
} catch {
|
|
293
|
+
// Ignore close errors
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|