/** * Agentic QE v3 - Sublinear Coverage Analyzer * * This is the main implementation of ADR-003: Sublinear Algorithms for Coverage Analysis. * Uses HNSW (Hierarchical Navigable Small World) indexing for O(log n) coverage gap * detection across large codebases. * * Performance characteristics (per ADR-003): * | Codebase Size | Traditional O(n) | v3 O(log n) | Improvement | * |---------------|-----------------|-------------|-------------| * | 1,000 files | 1,000 ops | 10 ops | 100x | * | 10,000 files | 10,000 ops | 13 ops | 770x | * | 100,000 files | 100,000 ops | 17 ops | 5,900x | * * Success metrics target: <100ms gap detection on 100k files * * @module coverage-analysis/sublinear-analyzer */ import { Result, ok, err, Severity } from '../../../shared/types'; import { MemoryBackend } from '../../../kernel/interfaces'; import { CoverageData, FileCoverage, CoverageGaps, CoverageGap, SimilarPatterns, } from '../interfaces'; import { HNSWIndex, IHNSWIndex, HNSWSearchResult, HNSWIndexStats, CoverageVectorMetadata, createHNSWIndex, } from './hnsw-index'; import { CoverageEmbedder, ICoverageEmbedder, CoverageQuery, createCoverageEmbedder, } from './coverage-embedder'; // ============================================================================ // Sublinear Analyzer Configuration // ============================================================================ /** * Configuration for the sublinear coverage analyzer */ export interface SublinearAnalyzerConfig { /** Number of nearest neighbors to search (default: 10) */ searchK: number; /** Minimum coverage threshold to identify gaps (default: 80) */ coverageThreshold: number; /** Minimum risk score to include in results (default: 0.3) */ riskThreshold: number; /** Maximum results to return (default: 100) */ maxResults: number; /** Enable automatic index updates on analyze (default: true) */ autoIndex: boolean; /** Batch size for bulk operations (default: 100) */ batchSize: number; /** Vector dimensions for embeddings (default: 128) */ dimensions: number; } /** * Default analyzer configuration */ export const DEFAULT_ANALYZER_CONFIG: SublinearAnalyzerConfig = { searchK: 10, coverageThreshold: 80, riskThreshold: 0.3, maxResults: 100, autoIndex: true, batchSize: 100, dimensions: 128, }; // ============================================================================ // Sublinear Analyzer Interface // ============================================================================ /** * Interface for sublinear coverage analysis operations */ export interface ISublinearCoverageAnalyzer { /** Initialize the analyzer and HNSW index */ initialize(): Promise; /** Index coverage data for O(log n) search */ indexCoverageData(data: CoverageData): Promise; /** Find coverage gaps using O(log n) HNSW search */ findGapsSublinear(query: CoverageQuery): Promise>; /** Find similar coverage patterns using vector similarity */ findSimilarPatterns(gap: CoverageGap, k: number): Promise>; /** Detect high-risk coverage zones using embeddings */ detectRiskZones(threshold: number): Promise>; /** Get analyzer statistics */ getStats(): Promise; /** Clear all indexed data */ clearIndex(): Promise; } /** * Result of indexing operation */ export interface IndexingResult { /** Number of files indexed */ filesIndexed: number; /** Time taken in milliseconds */ indexingTimeMs: number; /** Number of vectors stored */ vectorsStored: number; /** Any errors encountered */ errors: string[]; } /** * High-risk coverage zone */ export interface RiskZone { /** File path */ file: string; /** Risk score (0-1) */ riskScore: number; /** Severity level */ severity: Severity; /** Number of uncovered lines */ uncoveredLines: number; /** Number of uncovered branches */ uncoveredBranches: number; /** Recommended actions */ recommendations: string[]; /** Similar files with same pattern */ similarFiles: string[]; } /** * Analyzer statistics */ export interface SublinearAnalyzerStats { /** Total vectors indexed */ totalVectors: number; /** Total files tracked */ totalFiles: number; /** Index size in bytes */ indexSizeBytes: number; /** Average search latency */ avgSearchLatencyMs: number; /** P95 search latency */ p95SearchLatencyMs: number; /** P99 search latency */ p99SearchLatencyMs: number; /** Total search operations */ searchOperations: number; /** Time since last index update */ lastIndexUpdateMs: number; /** Performance vs linear scan improvement factor */ performanceImprovement: number; } // ============================================================================ // Sublinear Coverage Analyzer Implementation // ============================================================================ /** * Sublinear Coverage Analyzer * * Implements O(log n) coverage gap detection using HNSW vector indexing. * This is the core implementation for ADR-003 performance targets. * * Key features: * - O(log n) gap detection via HNSW approximate nearest neighbor search * - Dense coverage embeddings capture coverage patterns * - Batch indexing for efficient bulk operations * - Risk zone detection using vector similarity clustering * - <100ms target for 100k file codebases * * @example * ```typescript * const analyzer = new SublinearCoverageAnalyzer(memoryBackend); * await analyzer.initialize(); * await analyzer.indexCoverageData(coverageData); * * // O(log n) gap detection * const gaps = await analyzer.findGapsSublinear({ maxLineCoverage: 60 }); * * // Find similar patterns * const similar = await analyzer.findSimilarPatterns(gap, 5); * ``` */ export class SublinearCoverageAnalyzer implements ISublinearCoverageAnalyzer { private readonly config: SublinearAnalyzerConfig; private readonly hnswIndex: IHNSWIndex; private readonly embedder: ICoverageEmbedder; private lastIndexUpdate: number = 0; private fileCount: number = 0; private initialized: boolean = false; constructor( private readonly memory: MemoryBackend, config: Partial = {} ) { this.config = { ...DEFAULT_ANALYZER_CONFIG, ...config }; this.hnswIndex = createHNSWIndex(memory, { dimensions: this.config.dimensions, namespace: 'coverage-sublinear', }); this.embedder = createCoverageEmbedder({ dimensions: this.config.dimensions, }); } /** * Initialize the analyzer */ async initialize(): Promise { if (this.initialized) return; // Load existing index stats to get file count try { const stats = await this.hnswIndex.getStats(); this.fileCount = stats.vectorCount; } catch { this.fileCount = 0; } this.initialized = true; } /** * Index coverage data for O(log n) search * * This method creates dense vector embeddings for each file's coverage * data and stores them in the HNSW index for efficient similarity search. * * Time complexity: O(n log n) for n files during indexing * Search complexity: O(log n) after indexing * * @param data - Coverage data to index * @returns Indexing result with statistics */ async indexCoverageData(data: CoverageData): Promise { const startTime = performance.now(); const errors: string[] = []; let vectorsStored = 0; // Process files in batches to avoid memory pressure for (let i = 0; i < data.files.length; i += this.config.batchSize) { const batch = data.files.slice(i, i + this.config.batchSize); const batchItems = batch.map((file) => { try { const result = this.embedder.embedFileCoverage(file); return { key: `file:${file.path}`, vector: result.vector, metadata: result.metadata, }; } catch (error) { errors.push(`Failed to embed ${file.path}: ${error}`); return null; } }).filter((item): item is NonNullable => item !== null); await this.hnswIndex.batchInsert(batchItems); vectorsStored += batchItems.length; } this.lastIndexUpdate = Date.now(); this.fileCount = data.files.length; const endTime = performance.now(); return { filesIndexed: data.files.length, indexingTimeMs: endTime - startTime, vectorsStored, errors, }; } /** * Find coverage gaps using O(log n) HNSW search * * This is the core sublinear operation. Instead of scanning all files * linearly, we use vector similarity to find files matching the query * criteria efficiently. * * Performance: O(log n) operations for n indexed files * Target: <100ms for 100k files * * @param query - Query parameters for gap detection * @returns Coverage gaps matching the query */ async findGapsSublinear(query: CoverageQuery): Promise> { try { const startTime = performance.now(); // Create query embedding const queryResult = this.embedder.embedQuery(query); // Perform O(log n) HNSW search const searchResults = await this.hnswIndex.search( queryResult.vector, this.config.searchK * 2 // Search more to allow filtering ); // Filter results by query criteria const filteredResults = this.filterByQueryCriteria(searchResults, query); // Convert to coverage gaps const gaps = await this.convertToGaps(filteredResults, query); // Sort by risk score gaps.sort((a, b) => b.riskScore - a.riskScore); // Limit results const limitedGaps = gaps.slice(0, this.config.maxResults); const endTime = performance.now(); const searchTimeMs = endTime - startTime; // Calculate total uncovered lines const totalUncoveredLines = limitedGaps.reduce( (sum, gap) => sum + gap.lines.length, 0 ); // Estimate effort const estimatedEffort = this.estimateEffort(totalUncoveredLines); // Verify performance target if (searchTimeMs > 100) { console.warn( `Sublinear search took ${searchTimeMs.toFixed(2)}ms, ` + `exceeding 100ms target for ${this.fileCount} files` ); } return ok({ gaps: limitedGaps, totalUncoveredLines, estimatedEffort, }); } catch (error) { return err(error instanceof Error ? error : new Error(String(error))); } } /** * Find similar coverage patterns using vector similarity * * Uses HNSW to find files with similar coverage characteristics * to a given gap pattern. * * @param gap - Coverage gap to find similar patterns for * @param k - Number of similar patterns to return * @returns Similar patterns with similarity scores */ async findSimilarPatterns( gap: CoverageGap, k: number ): Promise> { try { const startTime = performance.now(); // Create gap embedding const embeddingResult = this.embedder.embedCoverageGap(gap); // O(log n) search for similar patterns const searchResults = await this.hnswIndex.search(embeddingResult.vector, k); // Convert to similar patterns format const patterns = searchResults.map((result) => ({ gap: this.createGapFromMetadata(result), similarity: result.score, })); const searchTime = performance.now() - startTime; return ok({ patterns, searchTime, }); } catch (error) { return err(error instanceof Error ? error : new Error(String(error))); } } /** * Detect high-risk coverage zones using clustering * * Identifies files with similar high-risk coverage patterns * that may require coordinated test improvements. * * @param threshold - Minimum risk score to include * @returns High-risk zones with similar files grouped */ async detectRiskZones(threshold: number): Promise> { try { // Create query for high-risk files const queryResult = this.embedder.embedQuery({ minRiskScore: threshold, maxLineCoverage: this.config.coverageThreshold, }); // Search for high-risk patterns const searchResults = await this.hnswIndex.search( queryResult.vector, this.config.maxResults ); // Group by similarity to identify risk zones const zones = this.clusterRiskZones(searchResults, threshold); return ok(zones); } catch (error) { return err(error instanceof Error ? error : new Error(String(error))); } } /** * Get analyzer statistics * * @returns Current statistics including performance metrics */ async getStats(): Promise { const indexStats = await this.hnswIndex.getStats(); // Calculate performance improvement vs linear scan // For HNSW: O(log n) vs O(n), improvement is approximately n / log2(n) const n = Math.max(1, indexStats.vectorCount); const log2n = Math.max(1, Math.log2(n)); const performanceImprovement = n / log2n; return { totalVectors: indexStats.vectorCount, totalFiles: this.fileCount, indexSizeBytes: indexStats.indexSizeBytes, avgSearchLatencyMs: indexStats.avgSearchLatencyMs, p95SearchLatencyMs: indexStats.p95SearchLatencyMs, p99SearchLatencyMs: indexStats.p99SearchLatencyMs, searchOperations: indexStats.searchOperations, lastIndexUpdateMs: this.lastIndexUpdate > 0 ? Date.now() - this.lastIndexUpdate : 0, performanceImprovement, }; } /** * Clear all indexed data */ async clearIndex(): Promise { await this.hnswIndex.clear(); this.fileCount = 0; this.lastIndexUpdate = 0; } // ============================================================================ // Private Helper Methods // ============================================================================ private filterByQueryCriteria( results: HNSWSearchResult[], query: CoverageQuery ): HNSWSearchResult[] { return results.filter((result) => { const metadata = result.metadata; if (!metadata) return false; // Apply coverage filters if (query.minLineCoverage !== undefined && metadata.lineCoverage < query.minLineCoverage) { return false; } if (query.maxLineCoverage !== undefined && metadata.lineCoverage > query.maxLineCoverage) { return false; } if (query.minBranchCoverage !== undefined && metadata.branchCoverage < query.minBranchCoverage) { return false; } if (query.maxBranchCoverage !== undefined && metadata.branchCoverage > query.maxBranchCoverage) { return false; } // Apply risk filters if (query.minRiskScore !== undefined && metadata.riskScore < query.minRiskScore) { return false; } if (query.maxRiskScore !== undefined && metadata.riskScore > query.maxRiskScore) { return false; } // Apply uncovered lines filter if (query.maxUncoveredLines !== undefined && metadata.uncoveredLineCount > query.maxUncoveredLines) { return false; } // Apply file pattern filter if (query.filePattern && !metadata.filePath.includes(query.filePattern)) { return false; } return true; }); } private async convertToGaps( results: HNSWSearchResult[], query: CoverageQuery ): Promise { return results.map((result) => { const metadata = result.metadata!; // Determine severity based on risk score const severity = this.riskScoreToSeverity(metadata.riskScore); // Generate recommendation const recommendation = this.generateRecommendation(metadata, severity); // Generate gap ID from file path const id = this.generateGapId(metadata.filePath); return { id, file: metadata.filePath, lines: this.generateUncoveredLineEstimate(metadata), branches: this.generateUncoveredBranchEstimate(metadata), riskScore: metadata.riskScore, severity, recommendation, }; }); } private createGapFromMetadata(result: HNSWSearchResult): CoverageGap { const metadata = result.metadata; if (!metadata) { return { id: result.key, file: 'unknown', lines: [], branches: [], riskScore: 0, severity: 'low', recommendation: 'Unable to retrieve metadata', }; } return { id: this.generateGapId(metadata.filePath), file: metadata.filePath, lines: this.generateUncoveredLineEstimate(metadata), branches: this.generateUncoveredBranchEstimate(metadata), riskScore: metadata.riskScore, severity: this.riskScoreToSeverity(metadata.riskScore), recommendation: this.generateRecommendation( metadata, this.riskScoreToSeverity(metadata.riskScore) ), }; } private clusterRiskZones( results: HNSWSearchResult[], threshold: number ): RiskZone[] { // Filter to high-risk only const highRisk = results.filter( (r) => r.metadata && r.metadata.riskScore >= threshold ); // Group by similarity (using simple threshold-based clustering) const zones: RiskZone[] = []; const processed = new Set(); for (const result of highRisk) { const metadata = result.metadata!; if (processed.has(metadata.filePath)) continue; processed.add(metadata.filePath); // Find similar files const similar = highRisk .filter( (r) => r.metadata && !processed.has(r.metadata.filePath) && r.score >= 0.7 // High similarity threshold ) .map((r) => r.metadata!.filePath); // Mark similar files as processed similar.forEach((f) => processed.add(f)); zones.push({ file: metadata.filePath, riskScore: metadata.riskScore, severity: this.riskScoreToSeverity(metadata.riskScore), uncoveredLines: metadata.uncoveredLineCount, uncoveredBranches: metadata.uncoveredBranchCount, recommendations: this.generateZoneRecommendations(metadata), similarFiles: similar, }); } // Sort by risk score zones.sort((a, b) => b.riskScore - a.riskScore); return zones; } private riskScoreToSeverity(riskScore: number): Severity { if (riskScore >= 0.8) return 'critical'; if (riskScore >= 0.6) return 'high'; if (riskScore >= 0.3) return 'medium'; return 'low'; } private generateRecommendation(metadata: CoverageVectorMetadata, severity: Severity): string { const uncovered = metadata.uncoveredLineCount; const file = metadata.filePath.split('/').pop() || metadata.filePath; let rec = `Add tests for ${uncovered} uncovered lines in ${file}`; if (severity === 'critical') { rec += '. CRITICAL: This file has very low coverage and high risk.'; } else if (severity === 'high') { rec += '. HIGH priority for test coverage.'; } if (metadata.uncoveredBranchCount > 5) { rec += ` Focus on ${metadata.uncoveredBranchCount} uncovered branches.`; } return rec; } private generateZoneRecommendations(metadata: CoverageVectorMetadata): string[] { const recommendations: string[] = []; if (metadata.riskScore >= 0.8) { recommendations.push( 'CRITICAL: Immediate test coverage required for this risk zone.' ); } if (metadata.lineCoverage < 50) { recommendations.push( 'Line coverage is below 50%. Add comprehensive unit tests.' ); } if (metadata.branchCoverage < 50) { recommendations.push( 'Branch coverage is below 50%. Add tests for conditional logic.' ); } if (metadata.uncoveredLineCount > 50) { recommendations.push( 'Consider breaking this file into smaller, more testable modules.' ); } return recommendations; } private generateUncoveredLineEstimate(metadata: CoverageVectorMetadata): number[] { // Generate estimated line numbers based on metadata // In production, this would be populated from actual coverage data const count = metadata.uncoveredLineCount; if (count === 0) return []; const totalLines = metadata.totalLines || 100; const coverage = metadata.lineCoverage / 100; // Distribute uncovered lines across the file const lines: number[] = []; const step = Math.max(1, Math.floor(totalLines / count)); for (let i = 0; i < count && lines.length < count; i++) { const line = Math.min(totalLines, (i + 1) * step); if (!lines.includes(line)) { lines.push(line); } } return lines; } private generateUncoveredBranchEstimate(metadata: CoverageVectorMetadata): number[] { const count = metadata.uncoveredBranchCount; if (count === 0) return []; const totalLines = metadata.totalLines || 100; const branches: number[] = []; const step = Math.max(1, Math.floor(totalLines / count)); for (let i = 0; i < count; i++) { branches.push(Math.min(totalLines, (i + 1) * step)); } return branches; } private generateGapId(filePath: string): string { const hash = filePath.split('').reduce((acc, char) => { const chr = char.charCodeAt(0); return ((acc << 5) - acc + chr) | 0; }, 0); return `gap-${Math.abs(hash).toString(16)}`; } private estimateEffort(uncoveredLines: number): number { // Estimate effort in hours // Assumes ~12 lines of test code per source line // and ~20 lines of test code per hour const testLinesNeeded = uncoveredLines * 12; return Math.ceil(testLinesNeeded / 20); } } // ============================================================================ // Factory Functions // ============================================================================ /** * Create a new sublinear coverage analyzer instance * * @param memory - Memory backend for storage * @param config - Optional configuration overrides * @returns Configured sublinear analyzer */ export function createSublinearAnalyzer( memory: MemoryBackend, config?: Partial ): SublinearCoverageAnalyzer { return new SublinearCoverageAnalyzer(memory, config); }