/** * 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 { type Declaring } from '../shared/savings.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: { /** * How much was walked -- ABSENT when the graph itself travels. * * In `circular`, `unused` and `impact` mode these are the only * statement of the size of what was analysed, because those replies * carry findings rather than the graph, so they are sent. * * In `graph` mode they are not: every one of them is a length over the * `graph` payload in the same response, so sending them asks the caller * to pay for arithmetic they can do on data they already have. * * Dropping them there also closed a disagreement. `externalDependencies` * counted the UNFILTERED graph while the payload beside it was the * filtered one, so with `includeExternal` off the reply stated a count * for data the caller never received -- the same defect the `rawGraph` * comment above describes, in the other direction. */ totalFiles?: number; analyzedFiles?: number; externalDependencies?: number; internalDependencies?: 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 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. */ /** * 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; /** * The walk matched no file, so there is no dependency graph to answer with. * * THIS USED TO ANSWER `{"nodes":[],"edges":[],"externalDependencies":[]}` * with `success: true`. A project has source files; an empty graph means the * targeting missed -- `files` naming something that is not source, a `cwd` * that is not the project root, or an `exclude` that swallowed the tree. * Dressed as an answer it reads as "this project imports nothing", which is a * stronger and wronger claim than "I found nothing to read". The bench * recorded one of these as a 93-99.7% token reduction: fourteen tokens * against a package.json, for a graph of nothing. * * The refusal names the scope AS WRITTEN, so the caller can see which part of * it missed. Those are the caller's own strings, returned to them; nothing * here is logged or transmitted. */ private scannedNothing; /** * Cache graph */ private cacheGraph; /** * Serialize graph for caching */ private serializeGraph; /** * Deserialize graph from cache */ private deserializeGraph; /** * How many analyses ran, and how many of them avoided the walk. * * IT ALSO PUBLISHED A LIFETIME SAVING, and that is gone. The two figures * were `totalTokensSaved`, summed from the per-call `savedTokens` this tool * used to record, and `averageReduction`, that sum over itself plus the * recorded input. Both inherited every error in the per-call numbers, and * those numbers came from an invented baseline -- so a project of 12,000 * files reported a saving of 790,200 tokens per call that would have been * identical had every file been empty. * * Nothing records a token field here any more, so the sums would now be a * constant zero, which reads as "saved nothing" rather than "not measured * here". The real figures are on the analytics rows, where both halves are * counted by the same party: see analytics/displaced-input.ts. */ getStats(): { totalAnalyses: number; cacheHits: number; incrementalUpdates: 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