/** * Smart Dependencies Tool - 83% Token Reduction * * Achieves token reduction through: * 1. Dependency graph caching (reuse across multiple queries) * 2. Incremental updates (only rebuild changed nodes) * 3. Compact graph representation (edges only, not full AST) * 4. Smart query modes (impact, circular, unused - return only what's needed) * 5. External vs internal separation (filter by relevance) * * Target: 83% reduction vs parsing and returning full file contents * * Week 5 - Phase 2 Track 2A */ import { CacheEngine } from '../../core/cache-engine.js'; import { TokenCounter } from '../../core/token-counter.js'; import { MetricsCollector } from '../../core/metrics.js'; import { type TruncationReason } from '../shared/bounded-traversal.js'; /** * Represents an import in a file */ export interface DependencyImport { source: string; specifiers: string[]; isExternal: boolean; isDynamic: boolean; line: number; } /** * Represents an export in a file */ export interface DependencyExport { name: string; type: 'named' | 'default' | 'namespace'; isReexport: boolean; source?: string; line: number; } /** * Node in the dependency graph */ export interface DependencyNode { file: string; hash: string; imports: DependencyImport[]; exports: DependencyExport[]; importedBy: string[]; importedByCount: number; lastAnalyzed: number; } /** * Circular dependency chain */ export interface CircularDependency { cycle: string[]; depth: number; severity: 'low' | 'medium' | 'high'; } /** * Unused import/export detection */ export interface UnusedDependency { file: string; type: 'import' | 'export'; name: string; source?: string; line: number; reason: string; } /** * Dependency impact analysis */ export interface DependencyImpact { file: string; directDependents: string[]; indirectDependents: string[]; totalImpact: number; criticalPath: string[][]; } export interface SmartDependenciesOptions { cwd?: string; files?: string[]; exclude?: string[]; mode?: 'graph' | 'circular' | 'unused' | 'impact'; targetFile?: string; includeExternal?: boolean; maxDepth?: number; useCache?: boolean; incrementalUpdate?: boolean; ttl?: number; format?: 'compact' | 'detailed'; includeMetadata?: boolean; /** * Wall-clock budget in ms for discovering the files to analyse. * * File discovery here was `globSync` over every source file in the tree, * which is the same unbounded walk issue #335 reported: on a large tree it * blocks the event loop until it finishes, and there was no point at which * it could give up and answer. Defaults to 10 s. */ deadlineMs?: number; } /** JSON-safe shape of the dependency graph. */ export interface DependencyGraphPayload { nodes: string[]; edges: Array<{ from: string; to: string; type: string; }>; externalDependencies: string[]; } export interface SmartDependenciesResult { success: boolean; mode: string; metadata: { totalFiles: number; analyzedFiles: number; externalDependencies: number; internalDependencies: number; tokensSaved: number; tokenCount: number; originalTokenCount: number; compressionRatio: number; duration: number; cacheHit: boolean; incrementalUpdate: boolean; /** * Set when a bound stopped file discovery, so the graph describes only * part of the tree. Absent means the walk ran to completion -- the * difference between "no cycles" and "no cycles among what I looked at". */ searchTruncated?: boolean; searchTruncatedBy?: TruncationReason; searchNote?: string; }; /** * The dependency graph, as data JSON can carry. * * THIS WAS A `Map`, and `JSON.stringify(new Map([...]))` is `{}` -- so every * response delivered `"graph": {}` no matter what was found. Measured on a * four-file fixture: metadata correctly reported analyzedFiles 4, * externalDependencies 2, internalDependencies 3, while the graph itself * arrived empty. The analysis was right and only the payload was lost, which * is why nothing ever looked broken from inside. * * The compact form was already being built to count tokens against, then * thrown away -- so the reported token count described data the caller never * received. */ graph?: DependencyGraphPayload; circular?: CircularDependency[]; unused?: UnusedDependency[]; impact?: DependencyImpact; error?: string; } export declare class SmartDependenciesTool { private cache; private tokenCounter; private metrics; /** * Token count per relative path, recorded while the file was open. * * Cleared at the start of every `analyze()` so it can never serve a count * from a previous call's content -- the saving is skipping a redundant read * WITHIN one analysis, not caching across them. */ private fileTokenCounts; /** * Whether a candidate module path is a file, remembered for one `analyze()`. * * Import resolution probes the same handful of candidates over and over -- * every file in a package resolving `./index` walks the identical extension * list -- and each probe was its own `existsSync`. Measured 2026-08-28 after * the read-once fix landed: 4.98 s, 14.6% of the run, and the single largest * remaining cost. Cleared per call for the same reason as the token counts. */ private pathExists; constructor(cache: CacheEngine, tokenCounter: TokenCounter, metrics: MetricsCollector); /** * Main entry point for dependency analysis * Alias for analyze() to maintain API consistency with other tools */ run(options?: SmartDependenciesOptions): Promise; /** * Core dependency analysis implementation */ analyze(options?: SmartDependenciesOptions): Promise; /** * Build dependency graph or load from cache */ private buildOrLoadGraph; /** * Build complete dependency graph */ private buildFullGraph; /** * Analyze a single file for dependencies */ private analyzeFile; /** * Extract imports from AST */ private extractImports; /** * Extract exports from AST */ private extractExports; /** * Build reverse dependencies (which files import this file) */ private buildReverseDependencies; /** * Detect files that have changed since last analysis */ private detectChangedFiles; /** * Incrementally update graph with changed files */ private incrementalGraphUpdate; /** * Detect circular dependencies */ private detectCircularDependencies; /** * Detect unused imports and exports */ private detectUnusedDependencies; /** * Analyze impact of changing a file */ private analyzeImpact; /** * Transform graph to compact output format */ private transformGraphOutput; /** * Create compact graph representation (edges only) */ private compactGraphRepresentation; /** * Utility: Check if dependency is external (node_modules) */ private isExternalDependency; /** * Utility: Resolve relative path */ /** * Whether a candidate resolves to an actual FILE, asked once per analysis. * * `isFile()`, not `existsSync`. A directory satisfies `existsSync`, so * `./foo` in a project holding a `foo/` directory resolved to the directory * itself -- producing a graph edge to `src/foo`, which is not a node in the * graph at all. Measured on a fixture holding both `foo.ts` and * `foo/index.ts`: the recorded edge was `src\\main.ts -> src\\foo`, pointing * at nothing, while Node resolves that import to `foo.ts`. */ private candidateIsFile; /** * Resolve an import specifier to a path relative to `cwd`. * * Order matches Node: the path as written, then the extension candidates, * then `index.*` inside a directory of that name. Each step returns on the * first hit, so a later candidate can never overwrite an earlier one. */ private resolveRelativePath; /** * Count external dependencies */ private countExternalDeps; /** * Count internal dependencies */ private countInternalDeps; /** * Estimate tokens for graph representation */ /** * What the graph this tool returns actually costs, tokenised. * * This multiplied: `graph.size * 50 + edges * 10`. Those constants were not * measured from anything, and they fed the headline `tokensSaved`. */ private measureGraphTokens; /** * Estimate tokens for full file contents */ /** * What reading these files would ACTUALLY have cost. * * THE BASELINE WAS INVENTED. * * This returned `files.length * 2000` -- an assumed 2,000 tokens per file, * measured from nothing. It was the baseline for every saving this tool * reported, so the analytics showed smart_dependencies saving 790,200 tokens * per call at 95.97%, a figure that would have been identical had the files * been empty. * * An overstated saving is the one number this project must never produce * (see tools/shared/savings.ts). So the files are read and counted. A file * that cannot be read contributes nothing rather than an assumed average -- * understating is the safe direction to be wrong in. */ private measureFullFileTokens; /** * Cache graph */ private cacheGraph; /** * Serialize graph for caching */ private serializeGraph; /** * Deserialize graph from cache */ private deserializeGraph; /** * Get dependency statistics */ getStats(): { totalAnalyses: number; cacheHits: number; incrementalUpdates: number; totalTokensSaved: number; averageReduction: number; }; } /** * Factory function for getting SmartDependenciesTool instance with injected dependencies */ export declare function getSmartDependenciesTool(cache: CacheEngine, tokenCounter: TokenCounter, metrics: MetricsCollector): SmartDependenciesTool; /** * CLI-friendly function for running smart dependencies analysis */ export declare function runSmartDependencies(options: SmartDependenciesOptions): Promise; /** * MCP Tool Definition */ export declare const SMART_DEPENDENCIES_TOOL_DEFINITION: { name: string; description: string; inputSchema: { type: string; properties: { cwd: { type: string; description: string; }; files: { type: string; items: { type: string; }; description: string; }; mode: { type: string; enum: string[]; description: string; default: string; }; targetFile: { type: string; description: string; }; includeExternal: { type: string; description: string; default: boolean; }; maxDepth: { type: string; description: string; }; useCache: { type: string; description: string; default: boolean; }; incrementalUpdate: { type: string; description: string; default: boolean; }; format: { type: string; enum: string[]; description: string; default: string; }; exclude: { type: string; items: { type: string; }; description: string; }; ttl: { type: string; description: string; default: number; }; includeMetadata: { type: string; description: string; default: boolean; }; deadlineMs: { type: string; description: string; }; }; }; }; //# sourceMappingURL=smart-dependencies.d.ts.map