feat(context): Introduce smart context system: analyzer, lazy loader, cache and README/docs improvements
This commit is contained in:
		
							
								
								
									
										191
									
								
								ts/context/lazy-file-loader.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								ts/context/lazy-file-loader.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,191 @@
 | 
			
		||||
import * as plugins from '../plugins.js';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import type { IFileMetadata, IFileInfo } from './types.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * LazyFileLoader handles efficient file loading by:
 | 
			
		||||
 * - Scanning files for metadata without loading contents
 | 
			
		||||
 * - Providing fast file size and token estimates
 | 
			
		||||
 * - Loading contents only when requested
 | 
			
		||||
 * - Parallel loading of selected files
 | 
			
		||||
 */
 | 
			
		||||
export class LazyFileLoader {
 | 
			
		||||
  private projectRoot: string;
 | 
			
		||||
  private metadataCache: Map<string, IFileMetadata> = new Map();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a new LazyFileLoader
 | 
			
		||||
   * @param projectRoot - Root directory of the project
 | 
			
		||||
   */
 | 
			
		||||
  constructor(projectRoot: string) {
 | 
			
		||||
    this.projectRoot = projectRoot;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Scans files in given globs and creates metadata without loading contents
 | 
			
		||||
   * @param globs - File patterns to scan (e.g., ['ts/**\/*.ts', 'test/**\/*.ts'])
 | 
			
		||||
   * @returns Array of file metadata
 | 
			
		||||
   */
 | 
			
		||||
  public async scanFiles(globs: string[]): Promise<IFileMetadata[]> {
 | 
			
		||||
    const metadata: IFileMetadata[] = [];
 | 
			
		||||
 | 
			
		||||
    for (const globPattern of globs) {
 | 
			
		||||
      try {
 | 
			
		||||
        const smartFiles = await plugins.smartfile.fs.fileTreeToObject(this.projectRoot, globPattern);
 | 
			
		||||
        const fileArray = Array.isArray(smartFiles) ? smartFiles : [smartFiles];
 | 
			
		||||
 | 
			
		||||
        for (const smartFile of fileArray) {
 | 
			
		||||
          try {
 | 
			
		||||
            const meta = await this.getMetadata(smartFile.path);
 | 
			
		||||
            metadata.push(meta);
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            // Skip files that can't be read
 | 
			
		||||
            console.warn(`Failed to get metadata for ${smartFile.path}:`, error.message);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        // Skip patterns that don't match any files
 | 
			
		||||
        console.warn(`No files found for pattern ${globPattern}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return metadata;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets metadata for a single file without loading contents
 | 
			
		||||
   * @param filePath - Absolute path to the file
 | 
			
		||||
   * @returns File metadata
 | 
			
		||||
   */
 | 
			
		||||
  public async getMetadata(filePath: string): Promise<IFileMetadata> {
 | 
			
		||||
    // Check cache first
 | 
			
		||||
    if (this.metadataCache.has(filePath)) {
 | 
			
		||||
      const cached = this.metadataCache.get(filePath)!;
 | 
			
		||||
      const currentStats = await fs.promises.stat(filePath);
 | 
			
		||||
 | 
			
		||||
      // Return cached if file hasn't changed
 | 
			
		||||
      if (cached.mtime === Math.floor(currentStats.mtimeMs)) {
 | 
			
		||||
        return cached;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get file stats
 | 
			
		||||
    const stats = await fs.promises.stat(filePath);
 | 
			
		||||
    const relativePath = plugins.path.relative(this.projectRoot, filePath);
 | 
			
		||||
 | 
			
		||||
    // Estimate tokens: rough estimate of ~4 characters per token
 | 
			
		||||
    // This is faster than reading and tokenizing the entire file
 | 
			
		||||
    const estimatedTokens = Math.ceil(stats.size / 4);
 | 
			
		||||
 | 
			
		||||
    const metadata: IFileMetadata = {
 | 
			
		||||
      path: filePath,
 | 
			
		||||
      relativePath,
 | 
			
		||||
      size: stats.size,
 | 
			
		||||
      mtime: Math.floor(stats.mtimeMs),
 | 
			
		||||
      estimatedTokens,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Cache the metadata
 | 
			
		||||
    this.metadataCache.set(filePath, metadata);
 | 
			
		||||
 | 
			
		||||
    return metadata;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads file contents for selected files in parallel
 | 
			
		||||
   * @param metadata - Array of file metadata to load
 | 
			
		||||
   * @param tokenizer - Function to calculate accurate token count
 | 
			
		||||
   * @returns Array of complete file info with contents
 | 
			
		||||
   */
 | 
			
		||||
  public async loadFiles(
 | 
			
		||||
    metadata: IFileMetadata[],
 | 
			
		||||
    tokenizer: (content: string) => number
 | 
			
		||||
  ): Promise<IFileInfo[]> {
 | 
			
		||||
    // Load files in parallel
 | 
			
		||||
    const loadPromises = metadata.map(async (meta) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const contents = await plugins.smartfile.fs.toStringSync(meta.path);
 | 
			
		||||
        const tokenCount = tokenizer(contents);
 | 
			
		||||
 | 
			
		||||
        const fileInfo: IFileInfo = {
 | 
			
		||||
          path: meta.path,
 | 
			
		||||
          relativePath: meta.relativePath,
 | 
			
		||||
          contents,
 | 
			
		||||
          tokenCount,
 | 
			
		||||
          importanceScore: meta.importanceScore,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return fileInfo;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.warn(`Failed to load file ${meta.path}:`, error.message);
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Wait for all loads to complete and filter out failures
 | 
			
		||||
    const results = await Promise.all(loadPromises);
 | 
			
		||||
    return results.filter((r): r is IFileInfo => r !== null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads a single file with contents
 | 
			
		||||
   * @param filePath - Absolute path to the file
 | 
			
		||||
   * @param tokenizer - Function to calculate accurate token count
 | 
			
		||||
   * @returns Complete file info with contents
 | 
			
		||||
   */
 | 
			
		||||
  public async loadFile(
 | 
			
		||||
    filePath: string,
 | 
			
		||||
    tokenizer: (content: string) => number
 | 
			
		||||
  ): Promise<IFileInfo> {
 | 
			
		||||
    const meta = await this.getMetadata(filePath);
 | 
			
		||||
    const contents = await plugins.smartfile.fs.toStringSync(filePath);
 | 
			
		||||
    const tokenCount = tokenizer(contents);
 | 
			
		||||
    const relativePath = plugins.path.relative(this.projectRoot, filePath);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      path: filePath,
 | 
			
		||||
      relativePath,
 | 
			
		||||
      contents,
 | 
			
		||||
      tokenCount,
 | 
			
		||||
      importanceScore: meta.importanceScore,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Updates importance scores for metadata entries
 | 
			
		||||
   * @param scores - Map of file paths to importance scores
 | 
			
		||||
   */
 | 
			
		||||
  public updateImportanceScores(scores: Map<string, number>): void {
 | 
			
		||||
    for (const [path, score] of scores) {
 | 
			
		||||
      const meta = this.metadataCache.get(path);
 | 
			
		||||
      if (meta) {
 | 
			
		||||
        meta.importanceScore = score;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Clears the metadata cache
 | 
			
		||||
   */
 | 
			
		||||
  public clearCache(): void {
 | 
			
		||||
    this.metadataCache.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets total estimated tokens for all cached metadata
 | 
			
		||||
   */
 | 
			
		||||
  public getTotalEstimatedTokens(): number {
 | 
			
		||||
    let total = 0;
 | 
			
		||||
    for (const meta of this.metadataCache.values()) {
 | 
			
		||||
      total += meta.estimatedTokens;
 | 
			
		||||
    }
 | 
			
		||||
    return total;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets cached metadata entries
 | 
			
		||||
   */
 | 
			
		||||
  public getCachedMetadata(): IFileMetadata[] {
 | 
			
		||||
    return Array.from(this.metadataCache.values());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user