feat(context): Introduce smart context system: analyzer, lazy loader, cache and README/docs improvements
This commit is contained in:
		
							
								
								
									
										391
									
								
								ts/context/context-analyzer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								ts/context/context-analyzer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,391 @@
 | 
			
		||||
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(', ');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user