392 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			392 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<IPrioritizationWeights>;
 | 
						|
  private tiers: Required<ITierConfig>;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Creates a new ContextAnalyzer
 | 
						|
   * @param projectRoot - Root directory of the project
 | 
						|
   * @param weights - Prioritization weights
 | 
						|
   * @param tiers - Tier configuration
 | 
						|
   */
 | 
						|
  constructor(
 | 
						|
    projectRoot: string,
 | 
						|
    weights: Partial<IPrioritizationWeights> = {},
 | 
						|
    tiers: Partial<ITierConfig> = {}
 | 
						|
  ) {
 | 
						|
    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<IAnalysisResult> {
 | 
						|
    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<Map<string, IFileDependencies>> {
 | 
						|
    const graph = new Map<string, IFileDependencies>();
 | 
						|
 | 
						|
    // 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<string, IFileDependencies>): void {
 | 
						|
    const damping = 0.85;
 | 
						|
    const iterations = 10;
 | 
						|
    const nodeCount = graph.size;
 | 
						|
 | 
						|
    // Initialize scores
 | 
						|
    const scores = new Map<string, number>();
 | 
						|
    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<string, number>();
 | 
						|
 | 
						|
      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<string, IFileDependencies>,
 | 
						|
    changedFiles: string[]
 | 
						|
  ): Promise<IFileAnalysis> {
 | 
						|
    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(', ');
 | 
						|
  }
 | 
						|
}
 |