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,288 @@
|
|
|
1
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
2
|
+
import type { EmbeddingGenerator } from '../../indexing/embeddings.js';
|
|
3
|
+
import type { ExistingFunction, FunctionIndex } from '../../types/documentation.js';
|
|
4
|
+
|
|
5
|
+
// Common function purpose keywords
|
|
6
|
+
const PURPOSE_KEYWORDS: Record<string, string[]> = {
|
|
7
|
+
authentication: ['auth', 'login', 'logout', 'token', 'session', 'jwt', 'credential'],
|
|
8
|
+
validation: ['validate', 'check', 'verify', 'assert', 'ensure', 'is', 'has'],
|
|
9
|
+
formatting: ['format', 'parse', 'stringify', 'serialize', 'convert', 'transform'],
|
|
10
|
+
fetching: ['fetch', 'get', 'load', 'retrieve', 'request', 'api', 'http'],
|
|
11
|
+
storage: ['save', 'store', 'persist', 'cache', 'set', 'put'],
|
|
12
|
+
utility: ['util', 'helper', 'tool', 'common', 'shared'],
|
|
13
|
+
error: ['error', 'exception', 'throw', 'catch', 'handle'],
|
|
14
|
+
logging: ['log', 'logger', 'debug', 'info', 'warn', 'error'],
|
|
15
|
+
date: ['date', 'time', 'moment', 'day', 'month', 'year', 'timestamp']
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class DuplicateDetector {
|
|
19
|
+
private tier2: Tier2Storage;
|
|
20
|
+
private embeddingGenerator: EmbeddingGenerator;
|
|
21
|
+
private functionIndex: Map<string, FunctionIndex> = new Map();
|
|
22
|
+
|
|
23
|
+
constructor(tier2: Tier2Storage, embeddingGenerator: EmbeddingGenerator) {
|
|
24
|
+
this.tier2 = tier2;
|
|
25
|
+
this.embeddingGenerator = embeddingGenerator;
|
|
26
|
+
this.buildFunctionIndex();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Build index of all functions in codebase
|
|
30
|
+
private buildFunctionIndex(): void {
|
|
31
|
+
const files = this.tier2.getAllFiles();
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const symbols = this.tier2.getSymbolsByFile(file.id);
|
|
35
|
+
|
|
36
|
+
for (const symbol of symbols) {
|
|
37
|
+
if (symbol.kind === 'function' || symbol.kind === 'method') {
|
|
38
|
+
const key = `${file.path}:${symbol.name}`;
|
|
39
|
+
const dependents = this.tier2.getFileDependents(file.path);
|
|
40
|
+
|
|
41
|
+
this.functionIndex.set(key, {
|
|
42
|
+
name: symbol.name,
|
|
43
|
+
file: file.path,
|
|
44
|
+
line: symbol.lineStart,
|
|
45
|
+
signature: symbol.signature || `${symbol.name}()`,
|
|
46
|
+
exported: symbol.exported,
|
|
47
|
+
usageCount: dependents.length + 1,
|
|
48
|
+
parameters: this.extractParameters(symbol.signature || ''),
|
|
49
|
+
returnType: this.extractReturnType(symbol.signature || ''),
|
|
50
|
+
docstring: symbol.docstring
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Find duplicate or similar functions
|
|
58
|
+
findDuplicates(code: string, threshold: number = 60): Array<FunctionIndex & { similarity: number }> {
|
|
59
|
+
const duplicates: Array<FunctionIndex & { similarity: number }> = [];
|
|
60
|
+
|
|
61
|
+
// Extract function name from code
|
|
62
|
+
const funcNameMatch = code.match(/(?:function|const|let|var)\s+(\w+)/);
|
|
63
|
+
const funcName = funcNameMatch ? funcNameMatch[1] : null;
|
|
64
|
+
|
|
65
|
+
// Extract purpose from code
|
|
66
|
+
const purpose = this.detectPurpose(code);
|
|
67
|
+
|
|
68
|
+
for (const [_key, func] of this.functionIndex) {
|
|
69
|
+
let similarity = 0;
|
|
70
|
+
|
|
71
|
+
// Check name similarity
|
|
72
|
+
if (funcName) {
|
|
73
|
+
const nameSimilarity = this.calculateNameSimilarity(funcName, func.name);
|
|
74
|
+
similarity += nameSimilarity * 0.3;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check purpose similarity
|
|
78
|
+
const funcPurpose = this.detectPurpose(func.signature + ' ' + (func.docstring || ''));
|
|
79
|
+
if (purpose && funcPurpose && purpose === funcPurpose) {
|
|
80
|
+
similarity += 40;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check code structure similarity
|
|
84
|
+
const structureSimilarity = this.calculateStructureSimilarity(code, func.signature);
|
|
85
|
+
similarity += structureSimilarity * 0.3;
|
|
86
|
+
|
|
87
|
+
if (similarity >= threshold) {
|
|
88
|
+
duplicates.push({
|
|
89
|
+
...func,
|
|
90
|
+
similarity: Math.round(similarity)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Sort by similarity
|
|
96
|
+
duplicates.sort((a, b) => b.similarity - a.similarity);
|
|
97
|
+
|
|
98
|
+
return duplicates.slice(0, 5);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Suggest existing functions based on intent
|
|
102
|
+
suggestExisting(intent: string, limit: number = 5): ExistingFunction[] {
|
|
103
|
+
const suggestions: Array<FunctionIndex & { relevance: number }> = [];
|
|
104
|
+
const intentLower = intent.toLowerCase();
|
|
105
|
+
const intentWords = intentLower.split(/\s+/);
|
|
106
|
+
|
|
107
|
+
for (const [_key, func] of this.functionIndex) {
|
|
108
|
+
if (!func.exported) continue;
|
|
109
|
+
|
|
110
|
+
let relevance = 0;
|
|
111
|
+
const funcLower = func.name.toLowerCase();
|
|
112
|
+
const docLower = (func.docstring || '').toLowerCase();
|
|
113
|
+
const combined = `${funcLower} ${docLower}`;
|
|
114
|
+
|
|
115
|
+
// Check direct name match
|
|
116
|
+
for (const word of intentWords) {
|
|
117
|
+
if (word.length < 3) continue;
|
|
118
|
+
|
|
119
|
+
if (funcLower.includes(word)) {
|
|
120
|
+
relevance += 30;
|
|
121
|
+
}
|
|
122
|
+
if (docLower.includes(word)) {
|
|
123
|
+
relevance += 20;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check purpose match
|
|
128
|
+
const purpose = this.detectPurpose(intent);
|
|
129
|
+
const funcPurpose = this.detectPurpose(combined);
|
|
130
|
+
if (purpose && funcPurpose && purpose === funcPurpose) {
|
|
131
|
+
relevance += 25;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Boost for commonly used functions
|
|
135
|
+
relevance += Math.min(func.usageCount * 2, 15);
|
|
136
|
+
|
|
137
|
+
if (relevance > 20) {
|
|
138
|
+
suggestions.push({ ...func, relevance });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sort by relevance
|
|
143
|
+
suggestions.sort((a, b) => b.relevance - a.relevance);
|
|
144
|
+
|
|
145
|
+
return suggestions.slice(0, limit).map(s => ({
|
|
146
|
+
name: s.name,
|
|
147
|
+
file: s.file,
|
|
148
|
+
line: s.line,
|
|
149
|
+
signature: s.signature,
|
|
150
|
+
description: s.docstring,
|
|
151
|
+
usageCount: s.usageCount,
|
|
152
|
+
purpose: this.detectPurpose(s.name + ' ' + (s.docstring || '')) || 'utility',
|
|
153
|
+
similarity: s.relevance
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Detect the purpose of code/function
|
|
158
|
+
private detectPurpose(text: string): string | null {
|
|
159
|
+
const lower = text.toLowerCase();
|
|
160
|
+
|
|
161
|
+
for (const [purpose, keywords] of Object.entries(PURPOSE_KEYWORDS)) {
|
|
162
|
+
if (keywords.some(k => lower.includes(k))) {
|
|
163
|
+
return purpose;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Calculate name similarity
|
|
171
|
+
private calculateNameSimilarity(name1: string, name2: string): number {
|
|
172
|
+
const lower1 = name1.toLowerCase();
|
|
173
|
+
const lower2 = name2.toLowerCase();
|
|
174
|
+
|
|
175
|
+
// Exact match
|
|
176
|
+
if (lower1 === lower2) return 100;
|
|
177
|
+
|
|
178
|
+
// Contains
|
|
179
|
+
if (lower1.includes(lower2) || lower2.includes(lower1)) return 70;
|
|
180
|
+
|
|
181
|
+
// Token overlap
|
|
182
|
+
const tokens1 = this.camelCaseToTokens(name1);
|
|
183
|
+
const tokens2 = this.camelCaseToTokens(name2);
|
|
184
|
+
|
|
185
|
+
let matches = 0;
|
|
186
|
+
for (const t1 of tokens1) {
|
|
187
|
+
for (const t2 of tokens2) {
|
|
188
|
+
if (t1 === t2) matches++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const totalTokens = Math.max(tokens1.length, tokens2.length);
|
|
193
|
+
return totalTokens > 0 ? (matches / totalTokens) * 100 : 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Calculate structure similarity
|
|
197
|
+
private calculateStructureSimilarity(code1: string, code2: string): number {
|
|
198
|
+
// Extract structural elements
|
|
199
|
+
const struct1 = this.extractStructure(code1);
|
|
200
|
+
const struct2 = this.extractStructure(code2);
|
|
201
|
+
|
|
202
|
+
let matches = 0;
|
|
203
|
+
let total = 0;
|
|
204
|
+
|
|
205
|
+
for (const key of Object.keys(struct1) as Array<keyof typeof struct1>) {
|
|
206
|
+
total++;
|
|
207
|
+
if (struct1[key] === struct2[key]) matches++;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return total > 0 ? (matches / total) * 100 : 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Extract structural features
|
|
214
|
+
private extractStructure(code: string): {
|
|
215
|
+
hasAsync: boolean;
|
|
216
|
+
hasReturn: boolean;
|
|
217
|
+
hasTryCatch: boolean;
|
|
218
|
+
hasLoop: boolean;
|
|
219
|
+
hasConditional: boolean;
|
|
220
|
+
paramCount: number;
|
|
221
|
+
} {
|
|
222
|
+
return {
|
|
223
|
+
hasAsync: /async\s+/.test(code),
|
|
224
|
+
hasReturn: /return\s+/.test(code),
|
|
225
|
+
hasTryCatch: /try\s*\{/.test(code),
|
|
226
|
+
hasLoop: /(?:for|while|do)\s*[\(\{]/.test(code),
|
|
227
|
+
hasConditional: /if\s*\(/.test(code),
|
|
228
|
+
paramCount: (code.match(/\([^)]*\)/)?.[0]?.split(',').length || 0)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Split camelCase to tokens
|
|
233
|
+
private camelCaseToTokens(name: string): string[] {
|
|
234
|
+
return name
|
|
235
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
236
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
237
|
+
.toLowerCase()
|
|
238
|
+
.split(/\s+/)
|
|
239
|
+
.filter(t => t.length > 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Extract parameters from signature
|
|
243
|
+
private extractParameters(signature: string): string[] {
|
|
244
|
+
const match = signature.match(/\(([^)]*)\)/);
|
|
245
|
+
if (!match) return [];
|
|
246
|
+
|
|
247
|
+
return match[1]
|
|
248
|
+
.split(',')
|
|
249
|
+
.map(p => p.trim())
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.map(p => p.split(':')[0].trim());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Extract return type from signature
|
|
255
|
+
private extractReturnType(signature: string): string | undefined {
|
|
256
|
+
const match = signature.match(/\):\s*(.+)$/);
|
|
257
|
+
return match ? match[1].trim() : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Refresh the function index
|
|
261
|
+
refresh(): void {
|
|
262
|
+
this.functionIndex.clear();
|
|
263
|
+
this.buildFunctionIndex();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Get index statistics
|
|
267
|
+
getStats(): {
|
|
268
|
+
totalFunctions: number;
|
|
269
|
+
exportedFunctions: number;
|
|
270
|
+
byPurpose: Record<string, number>;
|
|
271
|
+
} {
|
|
272
|
+
let exportedFunctions = 0;
|
|
273
|
+
const byPurpose: Record<string, number> = {};
|
|
274
|
+
|
|
275
|
+
for (const [_key, func] of this.functionIndex) {
|
|
276
|
+
if (func.exported) exportedFunctions++;
|
|
277
|
+
|
|
278
|
+
const purpose = this.detectPurpose(func.name) || 'other';
|
|
279
|
+
byPurpose[purpose] = (byPurpose[purpose] || 0) + 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
totalFunctions: this.functionIndex.size,
|
|
284
|
+
exportedFunctions,
|
|
285
|
+
byPurpose
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Architecture Enforcement Module - Phase 10
|
|
2
|
+
export { ArchitectureEnforcement } from './architecture-enforcement.js';
|
|
3
|
+
export { PatternLibrary } from './pattern-library.js';
|
|
4
|
+
export { PatternLearner } from './pattern-learner.js';
|
|
5
|
+
export { PatternValidator } from './pattern-validator.js';
|
|
6
|
+
export { DuplicateDetector } from './duplicate-detector.js';
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
2
|
+
import type { PatternCategory, CodeExample, PatternRule } from '../../types/documentation.js';
|
|
3
|
+
import type { PatternLibrary } from './pattern-library.js';
|
|
4
|
+
|
|
5
|
+
// Pattern detection rules
|
|
6
|
+
const PATTERN_DETECTORS: Array<{
|
|
7
|
+
category: PatternCategory;
|
|
8
|
+
name: string;
|
|
9
|
+
detect: RegExp;
|
|
10
|
+
extractExample: (code: string) => string | null;
|
|
11
|
+
rules: PatternRule[];
|
|
12
|
+
}> = [
|
|
13
|
+
{
|
|
14
|
+
category: 'error_handling',
|
|
15
|
+
name: 'Try-Catch Pattern',
|
|
16
|
+
detect: /try\s*\{[\s\S]*?\}\s*catch\s*\(/,
|
|
17
|
+
extractExample: (code: string) => {
|
|
18
|
+
const match = code.match(/try\s*\{[\s\S]*?\}\s*catch\s*\([^)]*\)\s*\{[\s\S]*?\}/);
|
|
19
|
+
return match ? match[0] : null;
|
|
20
|
+
},
|
|
21
|
+
rules: [
|
|
22
|
+
{ rule: 'Use try-catch for error-prone operations', severity: 'warning' },
|
|
23
|
+
{ rule: 'Log errors with context', severity: 'warning' }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
category: 'api_call',
|
|
28
|
+
name: 'Fetch API Pattern',
|
|
29
|
+
detect: /fetch\s*\(|axios\.|api\./,
|
|
30
|
+
extractExample: (code: string) => {
|
|
31
|
+
const match = code.match(/(?:await\s+)?(?:fetch|axios\.\w+|api\.\w+)\s*\([^)]*\)[\s\S]*?(?:\.json\(\)|;)/);
|
|
32
|
+
return match ? match[0] : null;
|
|
33
|
+
},
|
|
34
|
+
rules: [
|
|
35
|
+
{ rule: 'Check response status', severity: 'critical' },
|
|
36
|
+
{ rule: 'Handle network errors', severity: 'critical' }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
category: 'component',
|
|
41
|
+
name: 'React Component Pattern',
|
|
42
|
+
detect: /(?:function|const)\s+\w+\s*(?::\s*React\.FC|\([^)]*\))\s*(?:=>|{)/,
|
|
43
|
+
extractExample: (code: string) => {
|
|
44
|
+
const match = code.match(/(?:interface|type)\s+\w*Props[\s\S]*?}[\s\S]*?(?:function|const)\s+\w+[\s\S]*?(?:return\s*\([\s\S]*?\);|\))/);
|
|
45
|
+
return match ? match[0] : null;
|
|
46
|
+
},
|
|
47
|
+
rules: [
|
|
48
|
+
{ rule: 'Define Props interface', severity: 'warning' },
|
|
49
|
+
{ rule: 'Use functional components', severity: 'info' }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
category: 'validation',
|
|
54
|
+
name: 'Input Validation Pattern',
|
|
55
|
+
detect: /if\s*\(\s*!?\w+\s*(?:===?|!==?)\s*(?:null|undefined|''|""|0)\s*\)/,
|
|
56
|
+
extractExample: (code: string) => {
|
|
57
|
+
const match = code.match(/if\s*\([^)]*(?:null|undefined)[^)]*\)\s*\{[^}]*\}/);
|
|
58
|
+
return match ? match[0] : null;
|
|
59
|
+
},
|
|
60
|
+
rules: [
|
|
61
|
+
{ rule: 'Validate inputs before use', severity: 'warning' },
|
|
62
|
+
{ rule: 'Use optional chaining for nested access', severity: 'info' }
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
category: 'data_fetching',
|
|
67
|
+
name: 'Async/Await Pattern',
|
|
68
|
+
detect: /async\s+(?:function|\([^)]*\)\s*=>)/,
|
|
69
|
+
extractExample: (code: string) => {
|
|
70
|
+
const match = code.match(/async\s+(?:function\s+\w+)?\s*\([^)]*\)\s*(?::\s*Promise<[^>]+>)?\s*\{[\s\S]*?await[\s\S]*?\}/);
|
|
71
|
+
return match ? match[0] : null;
|
|
72
|
+
},
|
|
73
|
+
rules: [
|
|
74
|
+
{ rule: 'Use async/await for asynchronous code', severity: 'info' },
|
|
75
|
+
{ rule: 'Always await promises', severity: 'warning' }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
category: 'logging',
|
|
80
|
+
name: 'Logging Pattern',
|
|
81
|
+
detect: /console\.(log|error|warn|info)|logger\./,
|
|
82
|
+
extractExample: (code: string) => {
|
|
83
|
+
const match = code.match(/(?:console|logger)\.\w+\s*\([^)]*\)/);
|
|
84
|
+
return match ? match[0] : null;
|
|
85
|
+
},
|
|
86
|
+
rules: [
|
|
87
|
+
{ rule: 'Use structured logging', severity: 'info' },
|
|
88
|
+
{ rule: 'Include context in log messages', severity: 'info' }
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
export class PatternLearner {
|
|
94
|
+
private tier2: Tier2Storage;
|
|
95
|
+
private patternLibrary: PatternLibrary;
|
|
96
|
+
|
|
97
|
+
constructor(tier2: Tier2Storage, patternLibrary: PatternLibrary) {
|
|
98
|
+
this.tier2 = tier2;
|
|
99
|
+
this.patternLibrary = patternLibrary;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Learn patterns from the entire codebase
|
|
103
|
+
learnFromCodebase(): {
|
|
104
|
+
patternsLearned: number;
|
|
105
|
+
examplesAdded: number;
|
|
106
|
+
categories: Record<string, number>;
|
|
107
|
+
} {
|
|
108
|
+
const files = this.tier2.getAllFiles();
|
|
109
|
+
const categories: Record<string, number> = {};
|
|
110
|
+
let patternsLearned = 0;
|
|
111
|
+
let examplesAdded = 0;
|
|
112
|
+
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
if (!file.preview) continue;
|
|
115
|
+
|
|
116
|
+
const filePatterns = this.detectPatterns(file.preview);
|
|
117
|
+
|
|
118
|
+
for (const detected of filePatterns) {
|
|
119
|
+
categories[detected.category] = (categories[detected.category] || 0) + 1;
|
|
120
|
+
|
|
121
|
+
// Check if we already have this pattern
|
|
122
|
+
const existingPatterns = this.patternLibrary.getPatternsByCategory(detected.category);
|
|
123
|
+
const existing = existingPatterns.find(p => p.name === detected.name);
|
|
124
|
+
|
|
125
|
+
if (existing) {
|
|
126
|
+
// Add as example if different enough
|
|
127
|
+
if (detected.example && !this.isDuplicate(existing.examples, detected.example)) {
|
|
128
|
+
this.patternLibrary.addExample(existing.id, {
|
|
129
|
+
code: detected.example,
|
|
130
|
+
explanation: `Extracted from ${file.path}`,
|
|
131
|
+
file: file.path
|
|
132
|
+
});
|
|
133
|
+
examplesAdded++;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Create new pattern
|
|
137
|
+
this.patternLibrary.addPattern(
|
|
138
|
+
detected.name,
|
|
139
|
+
detected.category,
|
|
140
|
+
`${detected.name} detected in codebase`,
|
|
141
|
+
detected.example ? [{
|
|
142
|
+
code: detected.example,
|
|
143
|
+
explanation: `Extracted from ${file.path}`,
|
|
144
|
+
file: file.path
|
|
145
|
+
}] : [],
|
|
146
|
+
[],
|
|
147
|
+
detected.rules
|
|
148
|
+
);
|
|
149
|
+
patternsLearned++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { patternsLearned, examplesAdded, categories };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Detect patterns in a code snippet
|
|
158
|
+
detectPatterns(code: string): Array<{
|
|
159
|
+
category: PatternCategory;
|
|
160
|
+
name: string;
|
|
161
|
+
example: string | null;
|
|
162
|
+
rules: PatternRule[];
|
|
163
|
+
}> {
|
|
164
|
+
const detected: Array<{
|
|
165
|
+
category: PatternCategory;
|
|
166
|
+
name: string;
|
|
167
|
+
example: string | null;
|
|
168
|
+
rules: PatternRule[];
|
|
169
|
+
}> = [];
|
|
170
|
+
|
|
171
|
+
for (const detector of PATTERN_DETECTORS) {
|
|
172
|
+
if (detector.detect.test(code)) {
|
|
173
|
+
detected.push({
|
|
174
|
+
category: detector.category,
|
|
175
|
+
name: detector.name,
|
|
176
|
+
example: detector.extractExample(code),
|
|
177
|
+
rules: detector.rules
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return detected;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Learn a specific pattern from user input
|
|
186
|
+
learnPattern(
|
|
187
|
+
code: string,
|
|
188
|
+
name: string,
|
|
189
|
+
description?: string,
|
|
190
|
+
category?: PatternCategory
|
|
191
|
+
): { success: boolean; patternId?: string; message: string } {
|
|
192
|
+
// Auto-detect category if not provided
|
|
193
|
+
const detectedCategory = category || this.inferCategory(code);
|
|
194
|
+
|
|
195
|
+
// Check for existing similar pattern
|
|
196
|
+
const existingPatterns = this.patternLibrary.searchPatterns(name);
|
|
197
|
+
if (existingPatterns.some(p => p.name.toLowerCase() === name.toLowerCase())) {
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
message: `Pattern "${name}" already exists. Use add_example to add to existing pattern.`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract rules from code
|
|
205
|
+
const rules = this.extractRules(code);
|
|
206
|
+
|
|
207
|
+
// Create the pattern
|
|
208
|
+
const pattern = this.patternLibrary.addPattern(
|
|
209
|
+
name,
|
|
210
|
+
detectedCategory,
|
|
211
|
+
description || `User-defined pattern: ${name}`,
|
|
212
|
+
[{
|
|
213
|
+
code,
|
|
214
|
+
explanation: 'User-provided example'
|
|
215
|
+
}],
|
|
216
|
+
[],
|
|
217
|
+
rules
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
patternId: pattern.id,
|
|
223
|
+
message: `Pattern "${name}" created successfully`
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Infer category from code
|
|
228
|
+
private inferCategory(code: string): PatternCategory {
|
|
229
|
+
for (const detector of PATTERN_DETECTORS) {
|
|
230
|
+
if (detector.detect.test(code)) {
|
|
231
|
+
return detector.category;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return 'custom';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Extract rules from code patterns
|
|
238
|
+
private extractRules(code: string): PatternRule[] {
|
|
239
|
+
const rules: PatternRule[] = [];
|
|
240
|
+
|
|
241
|
+
// Detect common patterns and generate rules
|
|
242
|
+
if (/try\s*\{/.test(code)) {
|
|
243
|
+
rules.push({ rule: 'Use try-catch for error handling', severity: 'warning' });
|
|
244
|
+
}
|
|
245
|
+
if (/catch\s*\([^)]*\)\s*\{[\s\S]*console\.error/.test(code)) {
|
|
246
|
+
rules.push({ rule: 'Log errors in catch blocks', severity: 'info' });
|
|
247
|
+
}
|
|
248
|
+
if (/async\s+/.test(code)) {
|
|
249
|
+
rules.push({ rule: 'Use async functions for asynchronous operations', severity: 'info' });
|
|
250
|
+
}
|
|
251
|
+
if (/await\s+/.test(code)) {
|
|
252
|
+
rules.push({ rule: 'Await all promises', severity: 'warning' });
|
|
253
|
+
}
|
|
254
|
+
if (/interface\s+\w+Props/.test(code)) {
|
|
255
|
+
rules.push({ rule: 'Define Props interfaces for components', severity: 'warning' });
|
|
256
|
+
}
|
|
257
|
+
if (/\?\.\w+/.test(code)) {
|
|
258
|
+
rules.push({ rule: 'Use optional chaining for safe property access', severity: 'info' });
|
|
259
|
+
}
|
|
260
|
+
if (/\?\?/.test(code)) {
|
|
261
|
+
rules.push({ rule: 'Use nullish coalescing for default values', severity: 'info' });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return rules;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if example is duplicate
|
|
268
|
+
private isDuplicate(examples: CodeExample[], newExample: string): boolean {
|
|
269
|
+
const normalized = this.normalizeCode(newExample);
|
|
270
|
+
return examples.some(e => this.normalizeCode(e.code) === normalized);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Normalize code for comparison
|
|
274
|
+
private normalizeCode(code: string): string {
|
|
275
|
+
return code
|
|
276
|
+
.replace(/\s+/g, ' ')
|
|
277
|
+
.replace(/['"`]/g, '"')
|
|
278
|
+
.trim()
|
|
279
|
+
.toLowerCase();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Get learning statistics
|
|
283
|
+
getStats(): {
|
|
284
|
+
totalPatterns: number;
|
|
285
|
+
byCategory: Record<string, number>;
|
|
286
|
+
topPatterns: Array<{ name: string; usageCount: number }>;
|
|
287
|
+
} {
|
|
288
|
+
const patterns = this.patternLibrary.getAllPatterns();
|
|
289
|
+
const byCategory: Record<string, number> = {};
|
|
290
|
+
|
|
291
|
+
for (const pattern of patterns) {
|
|
292
|
+
byCategory[pattern.category] = (byCategory[pattern.category] || 0) + 1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const topPatterns = patterns
|
|
296
|
+
.sort((a, b) => b.usageCount - a.usageCount)
|
|
297
|
+
.slice(0, 5)
|
|
298
|
+
.map(p => ({ name: p.name, usageCount: p.usageCount }));
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
totalPatterns: patterns.length,
|
|
302
|
+
byCategory,
|
|
303
|
+
topPatterns
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|