/** * Agentic QE v3 - Defect Pattern Learner Service * Learns and recognizes defect patterns from historical data * Uses NomicEmbedder for semantic embeddings */ import { v4 as uuidv4 } from 'uuid'; import { Result, ok, err } from '../../../shared/types'; import { MemoryBackend, VectorSearchResult } from '../../../kernel/interfaces'; import { LearnRequest, LearnedDefectPatterns, DefectPattern, DefectInfo, ClusterRequest, DefectClusters, DefectCluster, } from '../interfaces'; import { NomicEmbedder, IEmbeddingProvider, EMBEDDING_CONFIG } from '../../../shared/embeddings'; /** * Interface for the pattern learner service */ export interface IPatternLearnerService { learnPatterns(request: LearnRequest): Promise>; clusterDefects(request: ClusterRequest): Promise>; findSimilarDefects(defect: DefectInfo, limit?: number): Promise>; getPatternById(patternId: string): Promise; listPatterns(limit?: number): Promise; } /** * Configuration for the pattern learner */ export interface PatternLearnerConfig { minPatternFrequency: number; maxPatterns: number; clusterThreshold: number; embeddingDimension: number; patternNamespace: string; enableSemanticClustering: boolean; /** Optional embedder instance (defaults to NomicEmbedder with fallback) */ embedder?: IEmbeddingProvider; } const DEFAULT_CONFIG: PatternLearnerConfig = { minPatternFrequency: 2, maxPatterns: 50, clusterThreshold: 0.7, embeddingDimension: EMBEDDING_CONFIG.DIMENSIONS, patternNamespace: 'defect-intelligence:patterns', enableSemanticClustering: true, }; /** * Known defect pattern indicators */ const KNOWN_PATTERNS: Record = { 'null-pointer': { indicators: ['null', 'undefined', 'NullPointerException', 'TypeError: Cannot read'], prevention: 'Use null-safe operators and defensive programming', }, 'race-condition': { indicators: ['concurrent', 'async', 'race', 'timing', 'intermittent'], prevention: 'Implement proper synchronization and use atomic operations', }, 'memory-leak': { indicators: ['memory', 'leak', 'OutOfMemory', 'heap', 'gc'], prevention: 'Ensure proper resource cleanup and avoid circular references', }, 'off-by-one': { indicators: ['index', 'boundary', 'array', 'loop', 'range'], prevention: 'Validate array bounds and use inclusive/exclusive ranges consistently', }, 'input-validation': { indicators: ['validation', 'sanitize', 'input', 'injection', 'XSS'], prevention: 'Implement strict input validation and sanitization', }, 'resource-exhaustion': { indicators: ['timeout', 'resource', 'exhausted', 'limit', 'quota'], prevention: 'Implement rate limiting and resource pooling', }, 'state-corruption': { indicators: ['state', 'inconsistent', 'corrupt', 'invalid state'], prevention: 'Use immutable state patterns and proper state machines', }, 'encoding-issue': { indicators: ['encoding', 'charset', 'UTF', 'unicode', 'character'], prevention: 'Use consistent encoding throughout the application', }, }; /** * Pattern Learner Service Implementation * Learns and recognizes defect patterns using ML and heuristics */ export class PatternLearnerService implements IPatternLearnerService { private readonly config: PatternLearnerConfig; private readonly patternCache: Map = new Map(); private readonly embedder: IEmbeddingProvider; constructor( private readonly memory: MemoryBackend, config: Partial = {} ) { this.config = { ...DEFAULT_CONFIG, ...config }; // Use provided embedder or create NomicEmbedder with fallback enabled this.embedder = config.embedder ?? new NomicEmbedder({ enableFallback: true }); } /** * Learn patterns from a collection of defects */ async learnPatterns(request: LearnRequest): Promise> { try { const { defects, includeResolutions = false } = request; if (defects.length === 0) { return err(new Error('No defects provided for learning')); } const patterns: DefectPattern[] = []; let modelUpdated = false; // Extract patterns using multiple strategies const extractedPatterns = await this.extractPatterns(defects); // Merge with known patterns const mergedPatterns = this.mergeWithKnownPatterns(extractedPatterns); // Filter by frequency threshold for (const pattern of mergedPatterns) { if (pattern.frequency >= this.config.minPatternFrequency) { patterns.push(pattern); // Store learned pattern await this.storePattern(pattern); modelUpdated = true; } } // Calculate improvement estimate based on pattern coverage const improvementEstimate = this.calculateImprovementEstimate( defects.length, patterns ); // Store resolution mappings if requested if (includeResolutions) { await this.learnResolutions(defects, patterns); } return ok({ patterns: patterns.slice(0, this.config.maxPatterns), modelUpdated, improvementEstimate, }); } catch (error) { return err(error instanceof Error ? error : new Error(String(error))); } } /** * Cluster defects by similarity */ async clusterDefects(request: ClusterRequest): Promise> { try { const { defects, method, minClusterSize = 2 } = request; if (defects.length === 0) { return err(new Error('No defects provided for clustering')); } let clusters: DefectCluster[]; switch (method) { case 'semantic': clusters = await this.clusterBySemantic(defects, minClusterSize); break; case 'behavioral': clusters = await this.clusterByBehavior(defects, minClusterSize); break; case 'temporal': clusters = await this.clusterByTemporal(defects, minClusterSize); break; default: return err(new Error(`Unknown clustering method: ${method}`)); } // Identify outliers (defects not in any cluster) const clusteredDefects = new Set(clusters.flatMap((c) => c.defects)); const outliers = defects .filter((d) => !clusteredDefects.has(d.id)) .map((d) => d.id); // Calculate clustering metrics const clusteringMetrics = this.calculateClusteringMetrics( clusters, defects.length ); return ok({ clusters, outliers, clusteringMetrics, }); } catch (error) { return err(error instanceof Error ? error : new Error(String(error))); } } /** * Find defects similar to a given defect */ async findSimilarDefects( defect: DefectInfo, limit: number = 5 ): Promise> { try { // Generate embedding for the defect const embedding = await this.generateDefectEmbedding(defect); // Search for similar vectors const results: VectorSearchResult[] = await this.memory.vectorSearch( embedding, limit + 1 // +1 to exclude self ); const similarDefects: DefectInfo[] = []; for (const result of results) { if (result.score >= this.config.clusterThreshold) { const storedDefect = await this.memory.get(result.key); if (storedDefect && storedDefect.id !== defect.id) { similarDefects.push(storedDefect); } } } return ok(similarDefects.slice(0, limit)); } catch (error) { return err(error instanceof Error ? error : new Error(String(error))); } } /** * Get a pattern by ID */ async getPatternById(patternId: string): Promise { // Check cache first if (this.patternCache.has(patternId)) { return this.patternCache.get(patternId); } // Load from memory const patternKey = `${this.config.patternNamespace}:${patternId}`; const stored = await this.memory.get(patternKey); if (stored) { this.patternCache.set(patternId, stored); return stored; } return undefined; } /** * List all learned patterns */ async listPatterns(limit: number = this.config.maxPatterns): Promise { const patterns: DefectPattern[] = []; const keys = await this.memory.search(`${this.config.patternNamespace}:*`, limit * 2); for (const key of keys) { const pattern = await this.memory.get(key); if (pattern) { patterns.push(pattern); if (patterns.length >= limit) break; } } return patterns.sort((a, b) => b.frequency - a.frequency); } // ============================================================================ // Private Helper Methods // ============================================================================ private async extractPatterns(defects: DefectInfo[]): Promise { const patternCounts: Map = new Map(); for (const defect of defects) { const text = `${defect.title} ${defect.description}`.toLowerCase(); // Match against known patterns for (const [patternName, patternData] of Object.entries(KNOWN_PATTERNS)) { const matchingIndicators = patternData.indicators.filter((indicator) => text.includes(indicator.toLowerCase()) ); if (matchingIndicators.length > 0) { const existing = patternCounts.get(patternName); if (existing) { existing.defects.push(defect.id); existing.indicators.push( ...matchingIndicators.filter((i) => !existing.indicators.includes(i)) ); } else { patternCounts.set(patternName, { indicators: matchingIndicators, defects: [defect.id], }); } } } // Also look for custom patterns based on tags if (defect.tags) { for (const tag of defect.tags) { const tagPattern = `tag-${tag}`; const existing = patternCounts.get(tagPattern); if (existing) { existing.defects.push(defect.id); } else { patternCounts.set(tagPattern, { indicators: [tag], defects: [defect.id], }); } } } } // Convert to DefectPattern array const patterns: DefectPattern[] = []; for (const [name, data] of patternCounts) { const knownPattern = KNOWN_PATTERNS[name]; patterns.push({ id: uuidv4(), name: this.formatPatternName(name), indicators: [...new Set(data.indicators)], frequency: data.defects.length, prevention: knownPattern?.prevention || `Address ${name} issues proactively`, }); } return patterns; } private mergeWithKnownPatterns(extracted: DefectPattern[]): DefectPattern[] { const merged = [...extracted]; // Add known patterns that weren't detected but are commonly relevant for (const [name, data] of Object.entries(KNOWN_PATTERNS)) { const exists = merged.some( (p) => p.name.toLowerCase().includes(name.replace('-', ' ')) ); if (!exists) { merged.push({ id: uuidv4(), name: this.formatPatternName(name), indicators: data.indicators, frequency: 0, // Not yet observed prevention: data.prevention, }); } } return merged; } private formatPatternName(name: string): string { return name .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } private async storePattern(pattern: DefectPattern): Promise { const patternKey = `${this.config.patternNamespace}:${pattern.id}`; await this.memory.set(patternKey, pattern, { namespace: 'defect-intelligence', persist: true, }); // Store vector for semantic search if (this.config.enableSemanticClustering) { const embedding = await this.generatePatternEmbedding(pattern); await this.memory.storeVector(patternKey, embedding, { patternId: pattern.id, name: pattern.name, }); } this.patternCache.set(pattern.id, pattern); } private async learnResolutions( defects: DefectInfo[], patterns: DefectPattern[] ): Promise { // Group defects by pattern for (const pattern of patterns) { const relatedDefects = defects.filter((d) => { const text = `${d.title} ${d.description}`.toLowerCase(); return pattern.indicators.some((i) => text.includes(i.toLowerCase())); }); if (relatedDefects.length > 0) { await this.memory.set( `${this.config.patternNamespace}:resolutions:${pattern.id}`, { patternId: pattern.id, defectIds: relatedDefects.map((d) => d.id), learnedAt: new Date().toISOString(), }, { namespace: 'defect-intelligence', persist: true } ); } } } private calculateImprovementEstimate( totalDefects: number, patterns: DefectPattern[] ): number { if (totalDefects === 0) return 0; // Calculate what percentage of defects are covered by patterns const coveredDefects = patterns.reduce((sum, p) => sum + p.frequency, 0); const coverage = Math.min(1, coveredDefects / totalDefects); // Estimate improvement based on coverage and pattern quality const patternQuality = patterns.length > 0 ? patterns.reduce((sum, p) => sum + (p.prevention ? 0.1 : 0), 0) / patterns.length : 0; return coverage * 0.7 + patternQuality * 0.3; } private async clusterBySemantic( defects: DefectInfo[], minSize: number ): Promise { const clusters: Map = new Map(); // Generate embeddings for all defects const embeddings: { defect: DefectInfo; embedding: number[] }[] = []; for (const defect of defects) { const embedding = await this.generateDefectEmbedding(defect); embeddings.push({ defect, embedding }); // Store for future similarity searches await this.memory.storeVector( `${this.config.patternNamespace}:defect:${defect.id}`, embedding, { defectId: defect.id } ); } // Simple clustering by finding nearest neighbors const assigned = new Set(); for (const { defect, embedding } of embeddings) { if (assigned.has(defect.id)) continue; const clusterMembers: string[] = [defect.id]; assigned.add(defect.id); // Find similar defects for (const other of embeddings) { if (other.defect.id === defect.id || assigned.has(other.defect.id)) continue; const similarity = this.cosineSimilarity(embedding, other.embedding); if (similarity >= this.config.clusterThreshold) { clusterMembers.push(other.defect.id); assigned.add(other.defect.id); } } if (clusterMembers.length >= minSize) { const clusterId = uuidv4(); const commonFactors = this.findCommonFactors( defects.filter((d) => clusterMembers.includes(d.id)) ); clusters.set(clusterId, { id: clusterId, label: this.generateClusterLabel( defects.filter((d) => clusterMembers.includes(d.id)) ), defects: clusterMembers, commonFactors, suggestedFix: this.suggestFix(commonFactors), }); } } return Array.from(clusters.values()); } private async clusterByBehavior( defects: DefectInfo[], minSize: number ): Promise { const clusters: Map = new Map(); // Group by tags and file const groups: Map = new Map(); for (const defect of defects) { const groupKey = defect.tags?.sort().join('|') || defect.file || 'unknown'; const group = groups.get(groupKey) || []; group.push(defect); groups.set(groupKey, group); } for (const [key, groupDefects] of groups) { if (groupDefects.length >= minSize) { const clusterId = uuidv4(); const commonFactors = this.findCommonFactors(groupDefects); clusters.set(clusterId, { id: clusterId, label: `Behavioral: ${key.replace('|', ', ')}`, defects: groupDefects.map((d) => d.id), commonFactors, suggestedFix: this.suggestFix(commonFactors), }); } } return Array.from(clusters.values()); } private async clusterByTemporal( defects: DefectInfo[], _minSize: number ): Promise { // Temporal clustering would require timestamps // Stub implementation - group by same title words const clusters: Map = new Map(); const titleWords: Map = new Map(); for (const defect of defects) { const words = defect.title.toLowerCase().split(/\s+/).slice(0, 2).join(' '); const group = titleWords.get(words) || []; group.push(defect); titleWords.set(words, group); } for (const [words, groupDefects] of titleWords) { if (groupDefects.length >= 2) { const clusterId = uuidv4(); const commonFactors = this.findCommonFactors(groupDefects); clusters.set(clusterId, { id: clusterId, label: `Temporal: ${words}`, defects: groupDefects.map((d) => d.id), commonFactors, suggestedFix: this.suggestFix(commonFactors), }); } } return Array.from(clusters.values()); } private findCommonFactors(defects: DefectInfo[]): string[] { const factors: string[] = []; // Find common tags if (defects.every((d) => d.tags)) { const allTags = defects.map((d) => new Set(d.tags)); const commonTags = [...allTags[0]].filter((tag) => allTags.every((tags) => tags.has(tag)) ); factors.push(...commonTags); } // Find common file patterns if (defects.every((d) => d.file)) { const filePaths = defects.map((d) => d.file!.split('/')); const minLen = Math.min(...filePaths.map((p) => p.length)); for (let i = 0; i < minLen; i++) { if (filePaths.every((p) => p[i] === filePaths[0][i])) { factors.push(`Path: ${filePaths[0][i]}`); } } } // Find common words in titles const titleWords = defects.map( (d) => new Set(d.title.toLowerCase().split(/\s+/)) ); const commonWords = [...titleWords[0]].filter( (word) => word.length > 3 && titleWords.every((words) => words.has(word)) ); factors.push(...commonWords.map((w) => `Keyword: ${w}`)); return [...new Set(factors)]; } private generateClusterLabel(defects: DefectInfo[]): string { // Generate a label based on common patterns const titles = defects.map((d) => d.title.toLowerCase()); const words = titles.flatMap((t) => t.split(/\s+/)); const wordCounts = new Map(); for (const word of words) { if (word.length > 3) { wordCounts.set(word, (wordCounts.get(word) || 0) + 1); } } const sortedWords = [...wordCounts.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 2) .map(([word]) => word); return sortedWords.join(' ') || 'Unnamed Cluster'; } private suggestFix(commonFactors: string[]): string { // Generate fix suggestion based on common factors for (const factor of commonFactors) { for (const [pattern, data] of Object.entries(KNOWN_PATTERNS)) { if ( factor.toLowerCase().includes(pattern) || data.indicators.some((i) => factor.toLowerCase().includes(i.toLowerCase()) ) ) { return data.prevention; } } } return 'Review common factors and implement targeted fixes'; } private calculateClusteringMetrics( clusters: DefectCluster[], totalDefects: number ): { silhouette: number; cohesion: number } { if (clusters.length === 0 || totalDefects === 0) { return { silhouette: 0, cohesion: 0 }; } // Simplified metrics const clusteredDefects = clusters.reduce( (sum, c) => sum + c.defects.length, 0 ); const avgClusterSize = clusteredDefects / clusters.length; // Cohesion: how many defects are clustered const cohesion = clusteredDefects / totalDefects; // Silhouette approximation: based on cluster size uniformity const sizes = clusters.map((c) => c.defects.length); const sizeVariance = sizes.reduce((sum, s) => sum + Math.pow(s - avgClusterSize, 2), 0) / sizes.length; const silhouette = Math.max(0, 1 - sizeVariance / (avgClusterSize * avgClusterSize || 1)); return { silhouette: Math.round(silhouette * 100) / 100, cohesion: Math.round(cohesion * 100) / 100, }; } /** * Generate embedding for a defect * Uses NomicEmbedder for semantic embeddings */ private async generateDefectEmbedding(defect: DefectInfo): Promise { const text = this.formatDefectForEmbedding(defect); return this.embedder.embed(text); } /** * Generate embedding for a pattern * Uses the same embedder as defects for consistent similarity matching */ private async generatePatternEmbedding(pattern: DefectPattern): Promise { const text = this.formatPatternForEmbedding(pattern); return this.embedder.embed(text); } /** * Format a defect for embedding generation */ private formatDefectForEmbedding(defect: DefectInfo): string { const parts = [ `Title: ${defect.title}`, defect.description ? `Description: ${defect.description}` : '', defect.tags?.length ? `Tags: ${defect.tags.join(', ')}` : '', defect.file ? `File: ${defect.file}` : '', ].filter(Boolean); return parts.join('\n'); } /** * Format a pattern for embedding generation */ private formatPatternForEmbedding(pattern: DefectPattern): string { const parts = [ `Pattern: ${pattern.name}`, `Indicators: ${pattern.indicators.join(', ')}`, `Prevention: ${pattern.prevention}`, `Frequency: ${pattern.frequency}`, ].filter(Boolean); return parts.join('\n'); } private cosineSimilarity(a: number[], b: number[]): number { if (a.length !== b.length) return 0; let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } const denominator = Math.sqrt(normA) * Math.sqrt(normB); return denominator === 0 ? 0 : dotProduct / denominator; } }