import * as plugins from '../plugins.js'; import type { IFileMetadata, IFileDependencies, IFileAnalysis, IAnalysisResult, TaskType, IPrioritizationWeights, ITierConfig, } from './types.js'; /** * ContextAnalyzer provides intelligent file selection and prioritization * based on dependency analysis, task relevance, and configurable weights */ export class ContextAnalyzer { private projectRoot: string; private weights: Required; private tiers: Required; /** * Creates a new ContextAnalyzer * @param projectRoot - Root directory of the project * @param weights - Prioritization weights * @param tiers - Tier configuration */ constructor( projectRoot: string, weights: Partial = {}, tiers: Partial = {} ) { this.projectRoot = projectRoot; // Default weights this.weights = { dependencyWeight: weights.dependencyWeight ?? 0.3, relevanceWeight: weights.relevanceWeight ?? 0.4, efficiencyWeight: weights.efficiencyWeight ?? 0.2, recencyWeight: weights.recencyWeight ?? 0.1, }; // Default tiers this.tiers = { essential: tiers.essential ?? { minScore: 0.8, trimLevel: 'none' }, important: tiers.important ?? { minScore: 0.5, trimLevel: 'light' }, optional: tiers.optional ?? { minScore: 0.2, trimLevel: 'aggressive' }, }; } /** * Analyzes files for a specific task type * @param metadata - Array of file metadata to analyze * @param taskType - Type of task being performed * @param changedFiles - Optional list of recently changed files (for commits) * @returns Analysis result with scored files */ public async analyze( metadata: IFileMetadata[], taskType: TaskType, changedFiles: string[] = [] ): Promise { const startTime = Date.now(); // Build dependency graph const dependencyGraph = await this.buildDependencyGraph(metadata); // Calculate centrality scores this.calculateCentrality(dependencyGraph); // Analyze each file const files: IFileAnalysis[] = []; for (const meta of metadata) { const analysis = await this.analyzeFile( meta, taskType, dependencyGraph, changedFiles ); files.push(analysis); } // Sort by importance score (highest first) files.sort((a, b) => b.importanceScore - a.importanceScore); const analysisDuration = Date.now() - startTime; return { taskType, files, dependencyGraph, totalFiles: metadata.length, analysisDuration, }; } /** * Builds a dependency graph from file metadata * @param metadata - Array of file metadata * @returns Dependency graph as a map */ private async buildDependencyGraph( metadata: IFileMetadata[] ): Promise> { const graph = new Map(); // Initialize graph entries for (const meta of metadata) { graph.set(meta.path, { path: meta.path, imports: [], importedBy: [], centrality: 0, }); } // Parse imports from each file for (const meta of metadata) { try { const contents = await plugins.smartfile.fs.toStringSync(meta.path); const imports = this.extractImports(contents, meta.path); const deps = graph.get(meta.path)!; deps.imports = imports; // Update importedBy for imported files for (const importPath of imports) { const importedDeps = graph.get(importPath); if (importedDeps) { importedDeps.importedBy.push(meta.path); } } } catch (error) { console.warn(`Failed to parse imports from ${meta.path}:`, error.message); } } return graph; } /** * Extracts import statements from file contents * @param contents - File contents * @param filePath - Path of the file being analyzed * @returns Array of absolute paths to imported files */ private extractImports(contents: string, filePath: string): string[] { const imports: string[] = []; const fileDir = plugins.path.dirname(filePath); // Match various import patterns const importRegex = /(?:import|export).*?from\s+['"](.+?)['"]/g; let match; while ((match = importRegex.exec(contents)) !== null) { const importPath = match[1]; // Skip external modules if (!importPath.startsWith('.')) { continue; } // Resolve relative import to absolute path let resolvedPath = plugins.path.resolve(fileDir, importPath); // Handle various file extensions const extensions = ['.ts', '.js', '.tsx', '.jsx', '/index.ts', '/index.js']; let found = false; for (const ext of extensions) { const testPath = resolvedPath.endsWith(ext) ? resolvedPath : resolvedPath + ext; try { // Use synchronous file check to avoid async in this context const fs = require('fs'); const exists = fs.existsSync(testPath); if (exists) { imports.push(testPath); found = true; break; } } catch (error) { // Continue trying other extensions } } if (!found && !resolvedPath.includes('.')) { // Try with .ts extension as default imports.push(resolvedPath + '.ts'); } } return imports; } /** * Calculates centrality scores for all nodes in the dependency graph * Uses a simplified PageRank-like algorithm * @param graph - Dependency graph */ private calculateCentrality(graph: Map): void { const damping = 0.85; const iterations = 10; const nodeCount = graph.size; // Initialize scores const scores = new Map(); for (const path of graph.keys()) { scores.set(path, 1.0 / nodeCount); } // Iterative calculation for (let i = 0; i < iterations; i++) { const newScores = new Map(); for (const [path, deps] of graph.entries()) { let score = (1 - damping) / nodeCount; // Add contributions from nodes that import this file for (const importerPath of deps.importedBy) { const importerDeps = graph.get(importerPath); if (importerDeps) { const importerScore = scores.get(importerPath) ?? 0; const outgoingCount = importerDeps.imports.length || 1; score += damping * (importerScore / outgoingCount); } } newScores.set(path, score); } // Update scores for (const [path, score] of newScores) { scores.set(path, score); } } // Normalize scores to 0-1 range const maxScore = Math.max(...scores.values()); if (maxScore > 0) { for (const deps of graph.values()) { const score = scores.get(deps.path) ?? 0; deps.centrality = score / maxScore; } } } /** * Analyzes a single file * @param meta - File metadata * @param taskType - Task being performed * @param graph - Dependency graph * @param changedFiles - Recently changed files * @returns File analysis */ private async analyzeFile( meta: IFileMetadata, taskType: TaskType, graph: Map, changedFiles: string[] ): Promise { const deps = graph.get(meta.path); const centralityScore = deps?.centrality ?? 0; // Calculate task-specific relevance const relevanceScore = this.calculateRelevance(meta, taskType); // Calculate efficiency (information per token) const efficiencyScore = this.calculateEfficiency(meta); // Calculate recency (for commit tasks) const recencyScore = this.calculateRecency(meta, changedFiles); // Calculate combined importance score const importanceScore = relevanceScore * this.weights.relevanceWeight + centralityScore * this.weights.dependencyWeight + efficiencyScore * this.weights.efficiencyWeight + recencyScore * this.weights.recencyWeight; // Assign tier const tier = this.assignTier(importanceScore); return { path: meta.path, relevanceScore, centralityScore, efficiencyScore, recencyScore, importanceScore, tier, reason: this.generateReason(meta, taskType, importanceScore, tier), }; } /** * Calculates task-specific relevance score */ private calculateRelevance(meta: IFileMetadata, taskType: TaskType): number { const relativePath = meta.relativePath.toLowerCase(); let score = 0.5; // Base score // README generation - prioritize public APIs and main exports if (taskType === 'readme') { if (relativePath.includes('index.ts')) score += 0.3; if (relativePath.match(/^ts\/[^\/]+\.ts$/)) score += 0.2; // Root level exports if (relativePath.includes('test/')) score -= 0.3; if (relativePath.includes('classes/')) score += 0.1; if (relativePath.includes('interfaces/')) score += 0.1; } // Commit messages - prioritize changed files and their dependencies if (taskType === 'commit') { if (relativePath.includes('test/')) score -= 0.2; // Recency will handle changed files } // Description generation - prioritize main exports and core interfaces if (taskType === 'description') { if (relativePath.includes('index.ts')) score += 0.4; if (relativePath.match(/^ts\/[^\/]+\.ts$/)) score += 0.3; if (relativePath.includes('test/')) score -= 0.4; if (relativePath.includes('interfaces/')) score += 0.2; } return Math.max(0, Math.min(1, score)); } /** * Calculates efficiency score (information density) */ private calculateEfficiency(meta: IFileMetadata): number { // Prefer files that are not too large (good signal-to-noise ratio) const optimalSize = 5000; // ~1250 tokens const distance = Math.abs(meta.estimatedTokens - optimalSize); const normalized = Math.max(0, 1 - distance / optimalSize); return normalized; } /** * Calculates recency score for changed files */ private calculateRecency(meta: IFileMetadata, changedFiles: string[]): number { if (changedFiles.length === 0) { return 0; } // Check if this file was changed const isChanged = changedFiles.some((changed) => changed === meta.path); return isChanged ? 1.0 : 0.0; } /** * Assigns a tier based on importance score */ private assignTier(score: number): 'essential' | 'important' | 'optional' | 'excluded' { if (score >= this.tiers.essential.minScore) return 'essential'; if (score >= this.tiers.important.minScore) return 'important'; if (score >= this.tiers.optional.minScore) return 'optional'; return 'excluded'; } /** * Generates a human-readable reason for the score */ private generateReason( meta: IFileMetadata, taskType: TaskType, score: number, tier: string ): string { const reasons: string[] = []; if (meta.relativePath.includes('index.ts')) { reasons.push('main export file'); } if (meta.relativePath.includes('test/')) { reasons.push('test file (lower priority)'); } if (taskType === 'readme' && meta.relativePath.match(/^ts\/[^\/]+\.ts$/)) { reasons.push('root-level module'); } reasons.push(`score: ${score.toFixed(2)}`); reasons.push(`tier: ${tier}`); return reasons.join(', '); } }