feat(context): Introduce smart context system: analyzer, lazy loader, cache and README/docs improvements
This commit is contained in:
		@@ -1,7 +1,10 @@
 | 
			
		||||
import * as plugins from '../plugins.js';
 | 
			
		||||
import type { ContextMode, IContextResult, IFileInfo, TaskType } from './types.js';
 | 
			
		||||
import type { ContextMode, IContextResult, IFileInfo, TaskType, IFileMetadata } from './types.js';
 | 
			
		||||
import { ContextTrimmer } from './context-trimmer.js';
 | 
			
		||||
import { ConfigManager } from './config-manager.js';
 | 
			
		||||
import { LazyFileLoader } from './lazy-file-loader.js';
 | 
			
		||||
import { ContextCache } from './context-cache.js';
 | 
			
		||||
import { ContextAnalyzer } from './context-analyzer.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Enhanced ProjectContext that supports context optimization strategies
 | 
			
		||||
@@ -10,6 +13,9 @@ export class EnhancedContext {
 | 
			
		||||
  private projectDir: string;
 | 
			
		||||
  private trimmer: ContextTrimmer;
 | 
			
		||||
  private configManager: ConfigManager;
 | 
			
		||||
  private lazyLoader: LazyFileLoader;
 | 
			
		||||
  private cache: ContextCache;
 | 
			
		||||
  private analyzer: ContextAnalyzer;
 | 
			
		||||
  private contextMode: ContextMode = 'trimmed';
 | 
			
		||||
  private tokenBudget: number = 190000; // Default for o4-mini
 | 
			
		||||
  private contextResult: IContextResult = {
 | 
			
		||||
@@ -29,6 +35,13 @@ export class EnhancedContext {
 | 
			
		||||
    this.projectDir = projectDirArg;
 | 
			
		||||
    this.configManager = ConfigManager.getInstance();
 | 
			
		||||
    this.trimmer = new ContextTrimmer(this.configManager.getTrimConfig());
 | 
			
		||||
    this.lazyLoader = new LazyFileLoader(projectDirArg);
 | 
			
		||||
    this.cache = new ContextCache(projectDirArg, this.configManager.getCacheConfig());
 | 
			
		||||
    this.analyzer = new ContextAnalyzer(
 | 
			
		||||
      projectDirArg,
 | 
			
		||||
      this.configManager.getPrioritizationWeights(),
 | 
			
		||||
      this.configManager.getTierConfig()
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
@@ -38,6 +51,7 @@ export class EnhancedContext {
 | 
			
		||||
    await this.configManager.initialize(this.projectDir);
 | 
			
		||||
    this.tokenBudget = this.configManager.getMaxTokens();
 | 
			
		||||
    this.trimmer.updateConfig(this.configManager.getTrimConfig());
 | 
			
		||||
    await this.cache.init();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
@@ -138,13 +152,28 @@ export class EnhancedContext {
 | 
			
		||||
    
 | 
			
		||||
    let totalTokenCount = 0;
 | 
			
		||||
    let totalOriginalTokens = 0;
 | 
			
		||||
    
 | 
			
		||||
    // Sort files by importance (for now just a simple alphabetical sort)
 | 
			
		||||
    // Later this could be enhanced with more sophisticated prioritization
 | 
			
		||||
    const sortedFiles = [...files].sort((a, b) => a.relative.localeCompare(b.relative));
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Convert SmartFile objects to IFileMetadata for analysis
 | 
			
		||||
    const metadata: IFileMetadata[] = files.map(sf => ({
 | 
			
		||||
      path: sf.path,
 | 
			
		||||
      relativePath: sf.relative,
 | 
			
		||||
      size: sf.contents.toString().length,
 | 
			
		||||
      mtime: Date.now(), // SmartFile doesn't expose mtime, use current time
 | 
			
		||||
      estimatedTokens: this.countTokens(sf.contents.toString()),
 | 
			
		||||
      importanceScore: 0
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    // Analyze files using ContextAnalyzer to get smart prioritization
 | 
			
		||||
    // (Note: This requires task type which we'll pass from buildContext)
 | 
			
		||||
    // For now, sort files by estimated tokens (smaller files first for better efficiency)
 | 
			
		||||
    const sortedFiles = [...files].sort((a, b) => {
 | 
			
		||||
      const aTokens = this.countTokens(a.contents.toString());
 | 
			
		||||
      const bTokens = this.countTokens(b.contents.toString());
 | 
			
		||||
      return aTokens - bTokens;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const processedFiles: string[] = [];
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    for (const smartfile of sortedFiles) {
 | 
			
		||||
      // Calculate original token count
 | 
			
		||||
      const originalContent = smartfile.contents.toString();
 | 
			
		||||
@@ -215,6 +244,154 @@ ${processedContent}
 | 
			
		||||
    return context;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Convert files to context with smart analysis and prioritization
 | 
			
		||||
   * @param metadata - File metadata to analyze
 | 
			
		||||
   * @param taskType - Task type for context-aware prioritization
 | 
			
		||||
   * @param mode - Context mode to use
 | 
			
		||||
   * @returns Context string
 | 
			
		||||
   */
 | 
			
		||||
  public async convertFilesToContextWithAnalysis(
 | 
			
		||||
    metadata: IFileMetadata[],
 | 
			
		||||
    taskType: TaskType,
 | 
			
		||||
    mode: ContextMode = this.contextMode
 | 
			
		||||
  ): Promise<string> {
 | 
			
		||||
    // Reset context result
 | 
			
		||||
    this.contextResult = {
 | 
			
		||||
      context: '',
 | 
			
		||||
      tokenCount: 0,
 | 
			
		||||
      includedFiles: [],
 | 
			
		||||
      trimmedFiles: [],
 | 
			
		||||
      excludedFiles: [],
 | 
			
		||||
      tokenSavings: 0
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Analyze files for smart prioritization
 | 
			
		||||
    const analysis = await this.analyzer.analyze(metadata, taskType, []);
 | 
			
		||||
 | 
			
		||||
    // Sort files by importance score (highest first)
 | 
			
		||||
    const sortedAnalysis = [...analysis.files].sort(
 | 
			
		||||
      (a, b) => b.importanceScore - a.importanceScore
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Filter out excluded tier
 | 
			
		||||
    const relevantFiles = sortedAnalysis.filter(f => f.tier !== 'excluded');
 | 
			
		||||
 | 
			
		||||
    let totalTokenCount = 0;
 | 
			
		||||
    let totalOriginalTokens = 0;
 | 
			
		||||
    const processedFiles: string[] = [];
 | 
			
		||||
 | 
			
		||||
    // Load files with cache support
 | 
			
		||||
    for (const fileAnalysis of relevantFiles) {
 | 
			
		||||
      try {
 | 
			
		||||
        // Check cache first
 | 
			
		||||
        let contents: string;
 | 
			
		||||
        let originalTokenCount: number;
 | 
			
		||||
 | 
			
		||||
        const cached = await this.cache.get(fileAnalysis.path);
 | 
			
		||||
        if (cached) {
 | 
			
		||||
          contents = cached.contents;
 | 
			
		||||
          originalTokenCount = cached.tokenCount;
 | 
			
		||||
        } else {
 | 
			
		||||
          // Load file
 | 
			
		||||
          const fileData = await plugins.smartfile.fs.toStringSync(fileAnalysis.path);
 | 
			
		||||
          contents = fileData;
 | 
			
		||||
          originalTokenCount = this.countTokens(contents);
 | 
			
		||||
 | 
			
		||||
          // Cache it
 | 
			
		||||
          await this.cache.set({
 | 
			
		||||
            path: fileAnalysis.path,
 | 
			
		||||
            contents,
 | 
			
		||||
            tokenCount: originalTokenCount,
 | 
			
		||||
            mtime: Date.now(),
 | 
			
		||||
            cachedAt: Date.now()
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        totalOriginalTokens += originalTokenCount;
 | 
			
		||||
 | 
			
		||||
        // Apply tier-based trimming
 | 
			
		||||
        let processedContent = contents;
 | 
			
		||||
        let trimLevel: 'none' | 'light' | 'aggressive' = 'light';
 | 
			
		||||
 | 
			
		||||
        if (fileAnalysis.tier === 'essential') {
 | 
			
		||||
          trimLevel = 'none';
 | 
			
		||||
        } else if (fileAnalysis.tier === 'important') {
 | 
			
		||||
          trimLevel = 'light';
 | 
			
		||||
        } else if (fileAnalysis.tier === 'optional') {
 | 
			
		||||
          trimLevel = 'aggressive';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Apply trimming based on mode and tier
 | 
			
		||||
        if (mode !== 'full' && trimLevel !== 'none') {
 | 
			
		||||
          const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
 | 
			
		||||
          processedContent = this.trimmer.trimFileWithLevel(
 | 
			
		||||
            relativePath,
 | 
			
		||||
            contents,
 | 
			
		||||
            trimLevel
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate token count
 | 
			
		||||
        const processedTokenCount = this.countTokens(processedContent);
 | 
			
		||||
 | 
			
		||||
        // Check token budget
 | 
			
		||||
        if (totalTokenCount + processedTokenCount > this.tokenBudget) {
 | 
			
		||||
          // We don't have budget for this file
 | 
			
		||||
          const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
 | 
			
		||||
          this.contextResult.excludedFiles.push({
 | 
			
		||||
            path: fileAnalysis.path,
 | 
			
		||||
            contents,
 | 
			
		||||
            relativePath,
 | 
			
		||||
            tokenCount: originalTokenCount,
 | 
			
		||||
            importanceScore: fileAnalysis.importanceScore
 | 
			
		||||
          });
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Format the file for context
 | 
			
		||||
        const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
 | 
			
		||||
        const formattedContent = `
 | 
			
		||||
====== START OF FILE ${relativePath} ======
 | 
			
		||||
 | 
			
		||||
${processedContent}
 | 
			
		||||
 | 
			
		||||
====== END OF FILE ${relativePath} ======
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        processedFiles.push(formattedContent);
 | 
			
		||||
        totalTokenCount += processedTokenCount;
 | 
			
		||||
 | 
			
		||||
        // Track file in appropriate list
 | 
			
		||||
        const fileInfo: IFileInfo = {
 | 
			
		||||
          path: fileAnalysis.path,
 | 
			
		||||
          contents: processedContent,
 | 
			
		||||
          relativePath,
 | 
			
		||||
          tokenCount: processedTokenCount,
 | 
			
		||||
          importanceScore: fileAnalysis.importanceScore
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (trimLevel === 'none' || processedContent === contents) {
 | 
			
		||||
          this.contextResult.includedFiles.push(fileInfo);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.contextResult.trimmedFiles.push(fileInfo);
 | 
			
		||||
          this.contextResult.tokenSavings += (originalTokenCount - processedTokenCount);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.warn(`Failed to process file ${fileAnalysis.path}:`, error.message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Join all processed files
 | 
			
		||||
    const context = processedFiles.join('\n');
 | 
			
		||||
 | 
			
		||||
    // Update context result
 | 
			
		||||
    this.contextResult.context = context;
 | 
			
		||||
    this.contextResult.tokenCount = totalTokenCount;
 | 
			
		||||
 | 
			
		||||
    return context;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Build context for the project
 | 
			
		||||
   * @param taskType Optional task type for task-specific context
 | 
			
		||||
@@ -233,42 +410,71 @@ ${processedContent}
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Gather files
 | 
			
		||||
    const taskConfig = taskType ? this.configManager.getTaskConfig(taskType) : undefined;
 | 
			
		||||
    const files = await this.gatherFiles(
 | 
			
		||||
      taskConfig?.includePaths,
 | 
			
		||||
      taskConfig?.excludePaths
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // Convert files to context
 | 
			
		||||
    // Create an array of all files to process
 | 
			
		||||
    const allFiles: plugins.smartfile.SmartFile[] = [];
 | 
			
		||||
    
 | 
			
		||||
    // Add individual files
 | 
			
		||||
    if (files.smartfilePackageJSON) allFiles.push(files.smartfilePackageJSON as plugins.smartfile.SmartFile);
 | 
			
		||||
    if (files.smartfilesReadme) allFiles.push(files.smartfilesReadme as plugins.smartfile.SmartFile);
 | 
			
		||||
    if (files.smartfilesReadmeHints) allFiles.push(files.smartfilesReadmeHints as plugins.smartfile.SmartFile);
 | 
			
		||||
    if (files.smartfilesNpmextraJSON) allFiles.push(files.smartfilesNpmextraJSON as plugins.smartfile.SmartFile);
 | 
			
		||||
    
 | 
			
		||||
    // Add arrays of files
 | 
			
		||||
    if (files.smartfilesMod) {
 | 
			
		||||
      if (Array.isArray(files.smartfilesMod)) {
 | 
			
		||||
        allFiles.push(...files.smartfilesMod);
 | 
			
		||||
      } else {
 | 
			
		||||
        allFiles.push(files.smartfilesMod);
 | 
			
		||||
    // Check if analyzer is enabled in config
 | 
			
		||||
    const analyzerConfig = this.configManager.getAnalyzerConfig();
 | 
			
		||||
    const useAnalyzer = analyzerConfig.enabled && taskType;
 | 
			
		||||
 | 
			
		||||
    if (useAnalyzer) {
 | 
			
		||||
      // Use new smart context building with lazy loading and analysis
 | 
			
		||||
      const taskConfig = this.configManager.getTaskConfig(taskType!);
 | 
			
		||||
 | 
			
		||||
      // Build globs for scanning
 | 
			
		||||
      const includeGlobs = taskConfig?.includePaths?.map(p => `${p}/**/*.ts`) || [
 | 
			
		||||
        'ts/**/*.ts',
 | 
			
		||||
        'ts*/**/*.ts'
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      // Add config files
 | 
			
		||||
      const configGlobs = [
 | 
			
		||||
        'package.json',
 | 
			
		||||
        'readme.md',
 | 
			
		||||
        'readme.hints.md',
 | 
			
		||||
        'npmextra.json'
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      // Scan files for metadata (fast, doesn't load contents)
 | 
			
		||||
      const metadata = await this.lazyLoader.scanFiles([...configGlobs, ...includeGlobs]);
 | 
			
		||||
 | 
			
		||||
      // Use analyzer to build context with smart prioritization
 | 
			
		||||
      await this.convertFilesToContextWithAnalysis(metadata, taskType!, this.contextMode);
 | 
			
		||||
    } else {
 | 
			
		||||
      // Fall back to old method for backward compatibility
 | 
			
		||||
      const taskConfig = taskType ? this.configManager.getTaskConfig(taskType) : undefined;
 | 
			
		||||
      const files = await this.gatherFiles(
 | 
			
		||||
        taskConfig?.includePaths,
 | 
			
		||||
        taskConfig?.excludePaths
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Convert files to context
 | 
			
		||||
      // Create an array of all files to process
 | 
			
		||||
      const allFiles: plugins.smartfile.SmartFile[] = [];
 | 
			
		||||
 | 
			
		||||
      // Add individual files
 | 
			
		||||
      if (files.smartfilePackageJSON) allFiles.push(files.smartfilePackageJSON as plugins.smartfile.SmartFile);
 | 
			
		||||
      if (files.smartfilesReadme) allFiles.push(files.smartfilesReadme as plugins.smartfile.SmartFile);
 | 
			
		||||
      if (files.smartfilesReadmeHints) allFiles.push(files.smartfilesReadmeHints as plugins.smartfile.SmartFile);
 | 
			
		||||
      if (files.smartfilesNpmextraJSON) allFiles.push(files.smartfilesNpmextraJSON as plugins.smartfile.SmartFile);
 | 
			
		||||
 | 
			
		||||
      // Add arrays of files
 | 
			
		||||
      if (files.smartfilesMod) {
 | 
			
		||||
        if (Array.isArray(files.smartfilesMod)) {
 | 
			
		||||
          allFiles.push(...files.smartfilesMod);
 | 
			
		||||
        } else {
 | 
			
		||||
          allFiles.push(files.smartfilesMod);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (files.smartfilesTest) {
 | 
			
		||||
      if (Array.isArray(files.smartfilesTest)) {
 | 
			
		||||
        allFiles.push(...files.smartfilesTest);
 | 
			
		||||
      } else {
 | 
			
		||||
        allFiles.push(files.smartfilesTest);
 | 
			
		||||
 | 
			
		||||
      if (files.smartfilesTest) {
 | 
			
		||||
        if (Array.isArray(files.smartfilesTest)) {
 | 
			
		||||
          allFiles.push(...files.smartfilesTest);
 | 
			
		||||
        } else {
 | 
			
		||||
          allFiles.push(files.smartfilesTest);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await this.convertFilesToContext(allFiles);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const context = await this.convertFilesToContext(allFiles);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    return this.contextResult;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user