import type { CodeChunk } from './types.js'; import type { RiskLevel } from './insights/types.js'; import type { RecoveryIndexes } from './dependent-count-index.js'; import type { InferredDependentMechanism } from './inferred-dependent-mechanisms.js'; /** * Risk level thresholds for dependent count. * Based on impact analysis: more dependents = higher risk of breaking changes. */ export declare const DEPENDENT_COUNT_THRESHOLDS: { readonly LOW: 5; readonly MEDIUM: 15; readonly HIGH: 30; }; /** * Complexity thresholds for risk assessment. * Based on cyclomatic complexity: higher complexity = harder to change safely. */ export declare const COMPLEXITY_THRESHOLDS: { readonly HIGH_COMPLEXITY_DEPENDENT: 10; readonly CRITICAL_AVG: 15; readonly CRITICAL_MAX: 25; readonly HIGH_AVG: 10; readonly HIGH_MAX: 20; readonly MEDIUM_AVG: 6; readonly MEDIUM_MAX: 15; }; export interface FileComplexityInfo { filepath: string; avgComplexity: number; maxComplexity: number; complexityScore: number; chunksWithComplexity: number; } /** * Result of `analyzeDependencies` below -- the chunk-only-index / complexity- * report consumer (`ComplexityAnalyzer`, `get_complexity`). See * `FindDependentsResult` further down for the richer, vectorDB-backed * `get_dependents` MCP tool's result shape; the two are deliberately * separate types -- `calculateOverallComplexityMetrics` (leaves * `complexityMetrics` `undefined` when there's no complexity data at all) is * part of what makes them distinct, see that function's own doc comment. */ export interface DependencyAnalysisResult { dependents: Array<{ filepath: string; isTestFile: boolean; }>; dependentCount: number; riskLevel: RiskLevel; complexityMetrics?: { averageComplexity: number; maxComplexity: number; filesWithComplexityData: number; highComplexityDependents: Array<{ filepath: string; maxComplexity: number; avgComplexity: number; }>; complexityRiskBoost: RiskLevel; }; } /** * Same field shape as `DependencyAnalysisResult['complexityMetrics']` above, * just always present rather than optional -- `findDependents`'s consumers * (the `get_dependents` MCP tool, `lien api-delta`) read * `complexityMetrics.maxComplexity` etc. unconditionally, so * `calculateComplexityMetricsOrDefault` (below * `calculateOverallComplexityMetrics`) substitutes an explicit all-zero/ * `'low'` default instead of `undefined` when there's no complexity data, * rather than pushing an optional-chaining burden onto every caller. */ export type ComplexityMetrics = NonNullable; /** * One (chunk, raw import specifier) pair in an import index bucket. #994 * Phase 3: the index used to store bare chunks, discarding both the raw * (pre-normalization) specifier and the fact that a bucket can span multiple * importer files -- so match-time code (`findDependentChunks`'s fuzzy loop) * had nothing to hand `importMatchesTarget` and had to re-derive the #887/ * #929 guards per chunk instead. Keeping `rawSpecifier` alongside each chunk * means every entry now carries both of `importMatchesTarget`'s required * inputs (the raw specifier and `chunk.metadata.file`), so match-time code * can call the single guarded primitive directly. See path-matching.ts:378 * for the resulting invariant. */ export interface ImportIndexEntry { chunk: T; rawSpecifier: string; } /** * Add the chunks in an import-index bucket to the dependent set via * `addChunk`, applying all three match-side guards (#884/#887/#929) through * `importMatchesTarget` -- one call per entry, using that entry's own * `rawSpecifier` and `chunk.metadata.file` (#994 Phase 3). Previously this * function received only a normalized specifier with no per-chunk importer * identity, so it had to reconstruct the #887/#929 guards itself via two * extra `matchesFile` calls (`ambiguous`/`pythonOnlyMatch`) instead of * calling the primitive directly -- see git history on this function for * that shape. Because each `ImportIndexEntry` now carries its own raw * specifier, a bucket spanning multiple importer files (and in principle * languages) is handled correctly for free: `importMatchesTarget` derives * each guard from that entry's own importer file, per entry, exactly like * every other match-side call site. */ export declare function addFuzzyMatchChunks(normalizedTarget: string, entries: ImportIndexEntry[], normalizePathCached: (path: string) => string, addChunk: (chunk: T) => void): void; /** * Finds all chunks that import the target file using index + fuzzy matching. * * @param normalizedTarget - The normalized path of the target file * @param importIndex - Index mapping import paths to (chunk, rawSpecifier) entries * @param normalizePathCached - Cached path normalization function, threaded * through to `importMatchesTarget` for the fuzzy match branch (#994) * @returns Array of chunks that import the target file (deduplicated) */ export declare function findDependentChunks(normalizedTarget: string, importIndex: Map[]>, normalizePathCached: (path: string) => string): T[]; /** * Check if a single chunk imports from the given source path. * Checks both `importedSymbols` keys and raw `imports` array. * * Uses `importMatchesTarget`, which applies the #884 whole-module guard * before `matchesFile` — see its doc comment in path-matching.ts (#886). */ export declare function chunkImportsFrom(chunk: CodeChunk, sourcePath: string, normalizePathCached: (path: string) => string): boolean; /** * Group chunks by their normalized file path. */ export declare function groupChunksByNormalizedPath(chunks: CodeChunk[], normalizePathCached: (path: string) => string): Map; /** * Find which symbols a file (given its chunks) genuinely re-exports from a * source path. * * Requires a non-empty intersection between (a) symbols the file imports from * the source and (b) symbols the file exports. A plain named import (`import * { x } from './a'`) paired with an unrelated own export (`export function y`) * is no longer a re-exporter — closing the #526 false positive. * * Wildcard markers (`'*'` from Rust `use foo::*`, `'* as x'` from JS * namespace imports) still trigger re-export detection: both mean "we pulled * in everything the source exports," so every own export counts as * re-exported. * * Single source of truth for this algorithm: both `fileIsReExporter` below * and `findDependents`'s own `buildReExportGraph` consume this (#532) -- * previously duplicated across the parser and CLI packages before * `findDependents` moved here; now a single in-module call either way. * * @returns The re-exported symbols; empty means the file is not a re-exporter. */ export declare function findReExportedSymbolsForFile(chunks: CodeChunk[], sourcePath: string, normalizePathCached: (path: string) => string): string[]; /** * Check if a file (given its chunks) genuinely re-exports anything from a * source path. Thin boolean wrapper over `findReExportedSymbolsForFile`. */ export declare function fileIsReExporter(chunks: CodeChunk[], sourcePath: string, normalizePathCached: (path: string) => string): boolean; /** * Find transitive dependents through re-export chains using BFS. * Bounded to MAX_REEXPORT_DEPTH. * * `reported` (output dedup) and `queued` (BFS-exploration dedup) are kept * as two separate sets -- see `processTransitiveChunk`'s doc comment for why * conflating them into one `visited` set made the result depend on BFS * traversal order (#1044). */ export declare function findTransitiveDependents(reExporterPaths: string[], importIndex: Map[]>, normalizedTarget: string, normalizePathCached: (path: string) => string, allChunksByFile: Map, existingFiles: Set): CodeChunk[]; /** * Analyzes dependencies for a given file by finding all chunks that import it. * * @param targetFilepath - The file to analyze dependencies for * @param allChunks - All chunks from the vector database * @param workspaceRoot - The workspace root directory * @returns Dependency analysis including dependents, count, and risk level */ export declare function analyzeDependencies(targetFilepath: string, allChunks: CodeChunk[], workspaceRoot: string): DependencyAnalysisResult; /** * A single usage of a symbol (call site). */ export interface SymbolUsage { /** The function/method that contains this call */ callerSymbol: string; /** Line number where the call occurs */ line: number; /** Code snippet showing the call */ snippet: string; } /** * Dependent file info, with optional symbol-level usages. */ export interface DependentInfo { filepath: string; isTestFile: boolean; /** Only present when symbol parameter is provided */ usages?: SymbolUsage[]; /** Depth at which this dependent was first discovered (1 = direct). */ hops?: number; /** * Present (`'inferred'`) only for a dependent recovered by a non-import * fallback instead of a real import edge -- absent for every ordinary, * import-verified dependent (the default, confident tier). A caller that * needs to distinguish "verified" from "recovered, lower confidence" * should filter on this field rather than assuming every entry in * `dependents` came from the import graph. * * Which fallbacks exist is NOT restated here: they are enumerated once, with * their canonical prose, in `./inferred-dependent-mechanisms.ts`, and named * per dependent by `inferredVia` below. This comment used to hand-list them * and was one of six surfaces that still said "C# only" after #1039 added a * second (see that module's doc for the measured consequence). */ confidence?: 'inferred'; /** * Which fallback recovered this dependent. Set if and only if `confidence` * is `'inferred'` -- both are written together by `inferredDependent()` * below, which is the only way either is produced. * * Exists so a consumer describing a recovered dependent can say which * mechanism ran instead of assuming one (#1018). Downstream prose must read * `INFERRED_DEPENDENT_MECHANISMS[inferredVia]` rather than restating it. */ inferredVia?: InferredDependentMechanism; } /** * The one constructor for a fallback-recovered dependent. * * Writing `confidence` and `inferredVia` together, in one place, is what keeps * the pair from drifting: there is no code path that can produce a * `confidence: 'inferred'` dependent whose mechanism is unknown, so a consumer * may rely on `inferredVia` being present whenever `confidence` is. A third * fallback (#1067) calls this with its own mechanism id and inherits every * prose surface for free. */ export declare function inferredDependent(filepath: string, mechanism: InferredDependentMechanism): DependentInfo; /** * Result of `findDependents` below. Generic over `` so * `chunksByFile`/`allChunks` preserve whatever chunk shape the caller fed * in (the CLI instantiates this at `T = SearchResult`, via its own * `DependencyAnalysisResult = FindDependentsResult` alias). */ export interface FindDependentsResult { dependents: DependentInfo[]; productionDependentCount: number; testDependentCount: number; chunksByFile: Map; fileComplexities: FileComplexityInfo[]; complexityMetrics: ComplexityMetrics; /** * Always `false`: `findDependents` reads its whole chunk set eagerly in * one pass (see `findDependents`'s own doc comment on `Iterable`), * never a paginated/truncated one. Kept on the result shape for API * stability with callers that already destructure it. */ hitLimit: boolean; allChunks: T[]; /** Total count of usages across all files (when symbol is specified) */ totalUsageCount?: number; /** True when BFS stopped because it hit the maxNodes cap. */ truncated: boolean; /** Count of production dependents that are NOT imported by any test file. */ uncoveredProductionDependents: number; /** * True when `symbol` was requested but couldn't be attributed at the * symbol level (it isn't a top-level export of the target file -- the * signature of a method or constructor, which no import statement in any * language names independently of its class -- see `buildDependentsList`), * so `dependents`/`riskLevel` were widened to the full file-level answer * instead of asserting an unverifiable symbol-scoped count. */ symbolAttributionDegraded?: boolean; /** * Only meaningful when `symbolAttributionDegraded` is `true`. Whether * `symbol` was found ANYWHERE among the target file's own indexed chunks * (as a chunk's own `symbolName` -- methods, constructors, and nested * functions/classes each get their own chunk -- or inside that chunk's * `symbols` bag), as opposed to a top-level export specifically. `true` * backs up the "likely a method or constructor" reading; `false` means the * name doesn't appear in this file's indexed chunks at all, which is just * as consistent with a typo, a hallucinated symbol, or one that used to * exist and was removed -- a caller wording the caveat should hedge * instead of asserting the method/constructor cause in that case. */ symbolFoundInFile?: boolean; /** * True for a SYMBOL-level query (`symbol` requested) where `symbol` * resolves to a real class/struct/interface/enum declaration in * `filepath` -- #1015. Distinct from (and mutually exclusive with) * `symbolAttributionDegraded`: that one fires when `symbol` is NOT a * top-level export at all (the shape of a method/constructor/typo, #931); * this one fires when `symbol` VERY MUCH IS a top-level export, just one * whose kind (a type) call-site tracking structurally cannot see through. * Nothing "calls" a type by its own name the way a function call does -- * constructor calls, type hints, `extends`/`implements` clauses, generic * type arguments, and dependency-injected property access don't reliably * surface as a tracked `callSite` -- so `totalUsageCount`/`dependents[].usages` * are a partial, best-effort floor here (often `0` even when real usages * exist), never a verified total, unlike a function/method symbol query * where `totalUsageCount` IS call-site-verified (see #1015's PHP * `formatPrice` reference case). `dependents`/`dependentCount` (which * files import the symbol) stay reliable either way -- only the * per-symbol usage count is in question. See `isTypeDeclarationSymbol`. */ typeSymbolAttributionIncomplete?: boolean; /** * True for a query -- file-level (no `symbol`) OR symbol-level -- that * came back with zero dependents for a language where * `hasDependentAttributionBlindSpot` is set (C#, Java, Kotlin, and Swift * as of #1005 -- see that predicate's doc comment for why each qualifies * for its own reason), EVEN AFTER attempting the type-reference-matching * recovery below (`dependentAttributionPartial`, still C#-only -- see * `enrichWithCSharpTypeReferenceDependents`). Those languages let a real * caller use `filepath`'s exports with no per-file import statement naming * it at all (C#'s `global using` / implicit enclosing-namespace member * access, #930; Java/Kotlin's same-package visibility; Swift's * whole-module access), so the import-graph scan this function runs has * no signal for that usage shape. `dependentCount: 0` / `riskLevel: "low"` * in this case means "neither scan found anything," not "nothing depends * on this file." * * Widened to symbol-level queries by #1097: until then, * `checkDependentAttributionIncomplete` unconditionally skipped this * determination whenever `symbol` was set, which is exactly the shape of * `get_dependents({filepath, symbol})` and every `lien api-delta` check -- * so a symbol-scoped query in one of these languages could report a bare, * uncaveated zero even when the file-level query on the identical file * correctly carried this same flag. Skipped for a symbol query when * `typeSymbolAttributionIncomplete` already explains the same zero (its * own, more specific caveat), so the two never contradict each other on * one response; no explicit exclusion is needed for * `symbolAttributionDegraded`, since that one only ever fires with a * nonzero final `dependents.length` (it widens to the file-level answer, * which requires at least one file to begin with) -- the zero-count guard * here already excludes it. */ dependentAttributionIncomplete?: boolean; /** * True for a FILE-level query (no `symbol`) where the import graph found * zero dependents but the C# type-reference-matching fallback * (`findCSharpTypeReferenceDependents`, #930's remaining half) recovered * one or more. Those recovered entries are tagged `confidence: 'inferred'` * on `DependentInfo` -- a word-boundary text match against a * uniquely-declared type name, not an import-verified edge -- so * `dependentCount`/`riskLevel` here are a recovered LOWER BOUND, not a * verified/complete answer: the heuristic can still miss a real dependent * that references the type via an alias, a generic type argument, or * reflection, none of which spell the bare type name in a matchable way. */ dependentAttributionPartial?: boolean; /** * False when the requested target has zero chunks anywhere in the scanned * index (#928) — i.e. it isn't a real file the indexer has seen, whether * because it was never indexed, is misspelled, or genuinely has no * extractable content. `matchesFile`'s fuzzy-matching strategies are tuned * to resolve real ambiguous specifiers (relative imports, namespace * prefixes, bare crate-root modules); they were never meant to stand in * for an existence check, and running them against a target with no * chunks of its own risks matching on textual coincidence alone (a * fabricated path silently inheriting an unrelated real file's entire * dependent graph — see the PHP `Command/Command.php` basename-collision * repro in #928). When `false`, `dependents`/`symbolAttributionDegraded`/ * `dependentAttributionIncomplete` above are moot -- `dependents` is * deliberately left empty rather than fuzzy-matched, and callers should * treat the whole result as "unresolved", not "confirmed zero dependents". */ targetIndexed: boolean; } /** * Find all files that depend on a target file, including transitive dependents * through re-export chains. Optionally tracks usages of a specific symbol. * * When `depth > 1`, the walk continues outward (BFS) over the import graph * using the same in-memory `importIndex`. Each newly discovered file is * tagged with the depth (hops) at which it was first reached. BFS stops when * `depth` is reached or `chunksByFile.size >= maxNodes` (sets `truncated`). * * Symbol-level queries (`symbol` set) always behave as depth=1 — transitive * symbol tracking through re-renaming chains is out of scope for this tool. * * Chunk-in/chunk-out and synchronous -- no dependency on a vectorDB. The * CLI's `get_dependents` MCP tool handler is a thin async wrapper around * this (see `packages/cli/src/mcp/handlers/dependency-analyzer.ts`): it * fetches (and caches) chunks via `vectorDB.scanAll()`, then calls this * function. `workspaceRoot` is a required, explicit parameter rather than * read from `process.cwd()` internally -- same reasoning as * `analyzeDependencies` above: keeps this function pure and independently * testable, with no hidden environment read. * * `recoveryIndexes` (#1101) is an optional, caller-threaded bag for the three * non-import recovery tiers' project-wide indexes (C# type-reference, Go * root-package, JVM same-package -- see the `enrichWith*Dependents` * functions below). Omit it (the default) for a one-off call -- this * function builds and discards a fresh, empty bag internally, identical to * today's behavior for every existing caller. A caller that invokes this * function in a loop over many FILE-LEVEL targets (no `symbol`) within ONE * outer batch should construct ONE `{}` bag before the loop and pass the * SAME object into every call -- each of the three tiers is then built at * most once per batch and reused for the rest of it, mirroring * `dependent-count-index.ts`'s own `RecoveryIndexes` batching discipline. * Note the "FILE-LEVEL" qualifier is load-bearing: all three * `enrichWith*Dependents` functions unconditionally no-op whenever `symbol` * is set (see their shared `if (symbol || ...)` guard), so a caller that * always queries with a `symbol` (`lien api-delta`'s `enrichDeltas`, which * always passes `change.symbolName`) never reaches this tier at all, * batched or not -- threading the bag through such a caller is still * correct and harmless, just not currently observable as a speedup there * (measured; see #1101's PR for the real before/after). Never a * module-level cache keyed by workspace root: see `jvm-source-root.ts`'s own * doc comment for why that goes stale under a long-running `lien serve` -- * this bag must stay scoped to one batch/call. */ export declare function findDependents(chunks: Iterable, filepath: string, log: (message: string, level?: 'warning') => void, workspaceRoot: string, symbol?: string, depth?: number, maxNodes?: number, /** * Surface the full normalized chunk set on the result. * Callers opt in by passing `true` only if they need the chunks * (e.g., the CLI annotator for test-association + complexity lookups). * Default `false` keeps memory cost down for the common MCP path. */ includeAllChunks?: boolean, recoveryIndexes?: RecoveryIndexes): FindDependentsResult; /** * Unpruned reference implementation of the `hasTestImporter` predicate: the * whole-import-index scan `countUncoveredProductionDependents` did before * #1075, expressed over a raw chunk set. * * Exists solely as the brute-force oracle the equivalence test checks the * fast path against -- the same role `computeDependentCountsBruteForce` plays * for `dependent-count-index.ts` (#1071). Production code must never call it: * it rebuilds the scan index per query and is O(every indexed import) per * question, which is precisely the cost #1075 removed. */ export declare function hasTestImporterBruteForce(chunks: Iterable, filepath: string, workspaceRoot: string): boolean; /** * The fast path `hasTestImporterBruteForce` is checked against, over the same * raw chunk-set input. Test-facing counterpart only -- `findDependents` builds * its scan index once per call and goes straight to `buildTestImporterIndex`. */ export declare function hasTestImporterFromChunks(chunks: Iterable, filepath: string, workspaceRoot: string): boolean; /** * What to call the caller when the calling chunk has no symbol name of its own. * * A `'block'` chunk is module-level code — top-level statements, a declaration * holding no function — so there is no enclosing function to name, and saying * so beats `'unknown'`, which reads as "we failed to work it out". Matches the * `(module-level)` wording `graph/dependency-graph.ts` (this package, since * lifted from `@liendev/review`) and `review`'s own `dependent-context.ts` * also use for the same situation — exported so those two sites (and this * one) share one implementation instead of three independently-maintained * copies of the same ternary (review finding on #1087: the * `dependency-graph.ts` copy had fallen out of sync with the other * two, still returning `'unknown'` for a module-level caller). Since #1087 * widened call-site extraction to module-level code this is a common case, * not a rare fallback. */ export declare function callerSymbolFor(chunk: CodeChunk): string; //# sourceMappingURL=dependency-analyzer.d.ts.map