/**
* extract_patterns tool implementation
*
* Extracts architectural and coding patterns with KG enrichment
*/
import { readdir, readFile } from 'fs/promises';
import { join, extname, relative } from 'path';
import { getDatabase } from '../knowledge-graph/database.js';
import { getYAGOResolver } from '../knowledge-graph/yago-resolver.js';
interface ExtractPatternsArgs {
path: string;
pattern_types?: string[];
min_occurrences?: number;
}
interface DetectedPattern {
type: string;
name: string;
occurrences: number;
examples: string[];
description?: string;
confidence: number;
kg_entities?: any[];
}
export async function extractPatterns(
args: ExtractPatternsArgs
): Promise<{ content: Array<{ type: string; text: string }> }> {
const { path, pattern_types = ['architectural', 'design', 'naming', 'testing'], min_occurrences = 3 } = args;
const patterns: Record<string, DetectedPattern[]> = {
architectural: [],
design: [],
naming: [],
testing: [],
};
// Scan codebase for patterns
const files = await getCodeFiles(path);
for (const file of files) {
const content = await readFile(file, 'utf-8');
const relPath = relative(path, file);
// Extract different pattern types
if (pattern_types.includes('architectural')) {
const archPatterns = extractArchitecturalPatterns(content, relPath);
patterns.architectural.push(...archPatterns);
}
if (pattern_types.includes('design')) {
const designPatterns = extractDesignPatterns(content, relPath);
patterns.design.push(...designPatterns);
}
if (pattern_types.includes('naming')) {
const namingPatterns = extractNamingPatterns(content, relPath);
patterns.naming.push(...namingPatterns);
}
if (pattern_types.includes('testing')) {
const testPatterns = extractTestingPatterns(content, relPath);
patterns.testing.push(...testPatterns);
}
}
// Aggregate and filter by min_occurrences
const aggregated: Record<string, DetectedPattern[]> = {};
for (const [type, typePatterns] of Object.entries(patterns)) {
const grouped = groupPatterns(typePatterns, min_occurrences);
aggregated[type] = grouped;
// Enrich with knowledge graph
for (const pattern of grouped) {
await enrichPatternWithKG(pattern);
}
}
// Store in database
await storePatternsInDB(aggregated);
// Format results
const results = formatPatternResults(aggregated);
return {
content: [
{
type: 'text',
text: results,
},
],
};
}
/**
* Get all code files in directory
*/
async function getCodeFiles(dir: string): Promise<string[]> {
const files: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
// Skip node_modules, .git, etc.
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await getCodeFiles(fullPath);
files.push(...subFiles);
} else if (entry.isFile()) {
const ext = extname(entry.name);
if (['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs'].includes(ext)) {
files.push(fullPath);
}
}
}
return files;
}
/**
* Extract architectural patterns
*/
function extractArchitecturalPatterns(content: string, file: string): DetectedPattern[] {
const patterns: DetectedPattern[] = [];
// MVC pattern
if (file.includes('/controllers/') || content.includes('Controller')) {
patterns.push({
type: 'architectural',
name: 'MVC',
occurrences: 1,
examples: [file],
confidence: 0.85,
});
}
// Repository pattern
if (file.includes('/repositories/') || content.match(/class \w+Repository/)) {
patterns.push({
type: 'architectural',
name: 'Repository Pattern',
occurrences: 1,
examples: [file],
confidence: 0.9,
});
}
// Service layer
if (file.includes('/services/') || content.match(/class \w+Service/)) {
patterns.push({
type: 'architectural',
name: 'Service Layer',
occurrences: 1,
examples: [file],
confidence: 0.85,
});
}
// Dependency injection
if (content.includes('inject(') || content.includes('@Injectable') || content.includes('constructor(')) {
patterns.push({
type: 'architectural',
name: 'Dependency Injection',
occurrences: 1,
examples: [file],
confidence: 0.8,
});
}
// Microservices
if (file.includes('/microservices/') || file.includes('/services/') && content.includes('express')) {
patterns.push({
type: 'architectural',
name: 'Microservices',
occurrences: 1,
examples: [file],
confidence: 0.75,
});
}
return patterns;
}
/**
* Extract design patterns
*/
function extractDesignPatterns(content: string, file: string): DetectedPattern[] {
const patterns: DetectedPattern[] = [];
// Singleton pattern
if (content.match(/private static instance|getInstance\(\)/)) {
patterns.push({
type: 'design',
name: 'Singleton',
occurrences: 1,
examples: [file],
confidence: 0.95,
});
}
// Factory pattern
if (content.match(/createFactory|Factory\.create|makeFactory/)) {
patterns.push({
type: 'design',
name: 'Factory',
occurrences: 1,
examples: [file],
confidence: 0.9,
});
}
// Observer pattern
if (content.match(/addEventListener|subscribe|on\(|emit\(/)) {
patterns.push({
type: 'design',
name: 'Observer',
occurrences: 1,
examples: [file],
confidence: 0.85,
});
}
// Decorator pattern
if (content.match(/@\w+\(|@decorator|\.decorator/)) {
patterns.push({
type: 'design',
name: 'Decorator',
occurrences: 1,
examples: [file],
confidence: 0.8,
});
}
// Strategy pattern
if (content.match(/Strategy|strategy\s*:/)) {
patterns.push({
type: 'design',
name: 'Strategy',
occurrences: 1,
examples: [file],
confidence: 0.75,
});
}
// Builder pattern
if (content.match(/\.build\(\)|Builder|\.with\w+\(/)) {
patterns.push({
type: 'design',
name: 'Builder',
occurrences: 1,
examples: [file],
confidence: 0.8,
});
}
return patterns;
}
/**
* Extract naming patterns
*/
function extractNamingPatterns(content: string, file: string): DetectedPattern[] {
const patterns: DetectedPattern[] = [];
// CamelCase
if (content.match(/[a-z][A-Z]/)) {
patterns.push({
type: 'naming',
name: 'camelCase',
occurrences: 1,
examples: [file],
confidence: 1.0,
});
}
// PascalCase
if (content.match(/class [A-Z][a-z]/)) {
patterns.push({
type: 'naming',
name: 'PascalCase',
occurrences: 1,
examples: [file],
confidence: 1.0,
});
}
// snake_case
if (content.match(/[a-z]+_[a-z]+/)) {
patterns.push({
type: 'naming',
name: 'snake_case',
occurrences: 1,
examples: [file],
confidence: 1.0,
});
}
// SCREAMING_SNAKE_CASE (constants)
if (content.match(/[A-Z]+_[A-Z]+/)) {
patterns.push({
type: 'naming',
name: 'SCREAMING_SNAKE_CASE',
occurrences: 1,
examples: [file],
confidence: 1.0,
});
}
return patterns;
}
/**
* Extract testing patterns
*/
function extractTestingPatterns(content: string, file: string): DetectedPattern[] {
const patterns: DetectedPattern[] = [];
// Unit tests
if (file.includes('.test.') || file.includes('.spec.')) {
patterns.push({
type: 'testing',
name: 'Unit Testing',
occurrences: 1,
examples: [file],
confidence: 1.0,
});
}
// describe/it pattern (BDD)
if (content.includes('describe(') && content.includes('it(')) {
patterns.push({
type: 'testing',
name: 'BDD (Behavior-Driven Development)',
occurrences: 1,
examples: [file],
confidence: 0.95,
});
}
// Test fixtures
if (content.match(/beforeEach|beforeAll|afterEach|afterAll|setUp|tearDown/)) {
patterns.push({
type: 'testing',
name: 'Test Fixtures',
occurrences: 1,
examples: [file],
confidence: 0.9,
});
}
// Mocking
if (content.match(/mock|stub|spy|jest\.fn|sinon/)) {
patterns.push({
type: 'testing',
name: 'Test Mocking',
occurrences: 1,
examples: [file],
confidence: 0.9,
});
}
return patterns;
}
/**
* Group patterns by name and count occurrences
*/
function groupPatterns(patterns: DetectedPattern[], minOccurrences: number): DetectedPattern[] {
const grouped = new Map<string, DetectedPattern>();
for (const pattern of patterns) {
const key = pattern.name;
if (grouped.has(key)) {
const existing = grouped.get(key)!;
existing.occurrences += pattern.occurrences;
existing.examples.push(...pattern.examples);
existing.confidence = Math.max(existing.confidence, pattern.confidence);
} else {
grouped.set(key, { ...pattern });
}
}
return Array.from(grouped.values())
.filter((p) => p.occurrences >= minOccurrences)
.sort((a, b) => b.occurrences - a.occurrences);
}
/**
* Enrich pattern with knowledge graph data
*/
async function enrichPatternWithKG(pattern: DetectedPattern): Promise<void> {
try {
const yagoResolver = getYAGOResolver();
const entities = await yagoResolver.resolveEntity(pattern.name, 2);
if (entities.length > 0) {
pattern.kg_entities = entities;
pattern.description = entities[0].description || pattern.description;
}
} catch (error) {
console.error(`Failed to enrich pattern ${pattern.name}:`, error);
}
}
/**
* Store patterns in database
*/
async function storePatternsInDB(patterns: Record<string, DetectedPattern[]>): Promise<void> {
const db = await getDatabase();
for (const [category, categoryPatterns] of Object.entries(patterns)) {
for (const pattern of categoryPatterns) {
try {
// Check if already exists
const existing = db.findProgrammingConceptByName(pattern.name);
if (!existing) {
await db.insertProgrammingConcept({
concept_name: pattern.name,
category,
description: pattern.description,
examples: pattern.examples.slice(0, 5),
metadata: {
occurrences: pattern.occurrences,
confidence: pattern.confidence,
kg_enriched: !!pattern.kg_entities,
},
});
}
} catch (error) {
console.error(`Failed to store pattern ${pattern.name}:`, error);
}
}
}
}
/**
* Format pattern results for display
*/
function formatPatternResults(patterns: Record<string, DetectedPattern[]>): string {
const lines: string[] = [];
lines.push('# Extracted Patterns');
lines.push('');
for (const [type, typePatterns] of Object.entries(patterns)) {
if (typePatterns.length === 0) continue;
lines.push(`## ${type.charAt(0).toUpperCase() + type.slice(1)} Patterns`);
lines.push('');
for (const pattern of typePatterns) {
lines.push(`### ${pattern.name}`);
lines.push(`- **Occurrences:** ${pattern.occurrences}`);
lines.push(`- **Confidence:** ${(pattern.confidence * 100).toFixed(0)}%`);
if (pattern.description) {
lines.push(`- **Description:** ${pattern.description}`);
}
if (pattern.kg_entities && pattern.kg_entities.length > 0) {
const entity = pattern.kg_entities[0];
lines.push(`- **Knowledge Graph:** ${entity.label}`);
if (entity.facts.length > 0) {
lines.push(` - ${entity.facts.length} related facts in YAGO`);
}
}
lines.push(`- **Examples:**`);
for (const example of pattern.examples.slice(0, 3)) {
lines.push(` - ${example}`);
}
lines.push('');
}
}
return lines.join('\n');
}