/** * Batch reverse-dependency counting: "how many other indexed files import * this one", for EVERY file in a chunk set, in one pass. * * ## Why this exists * * `findDependents` (dependency-analyzer.ts) is the authoritative per-target * reverse-dependency API, and it carries every guard the resolution wave * added -- #884 (whole-module imports), #887 (single-file vs. package-directory * specifiers), #929 (Python bare-module matching), #1028 (PHP namespace * matching), #1021/#1056 (Rust exact-single-file `mod`/crate-root specifiers), * plus the C# type-reference (#930/#943) and Go root-package (#1039) recovery * tiers. But it resolves ONE target per call and scans the whole import index * to do it, so calling it once per file is O(files x unique specifiers) with a * per-call floor measured in seconds. * * `search_code`'s ranking boost needs a count for every file, and needs it * cheap. Before #1071 it got that from a private ~40-line resolver that only * understood `./foo` and `../bar` specifiers -- so on any language whose * imports are dotted namespaces or module URLs (C#, Java, Kotlin, Swift, Go, * Rust) EVERY file scored 0, and `applyStructuralBoost` degraded to the exact * identity function. This module replaces it: same guarded matching decision * as `findDependents` (literally `importMatchesTarget`, not a re-derivation), * at batch cost. * * ## How the batch cost is achieved * * The naive inversion is a full cross product: every (specifier, file) pair * through `importMatchesTarget`. That is exactly what a per-file * `findDependents` loop pays, just rearranged. * * Instead this builds a **candidate index** keyed on a property that is * *necessary* for a match, looks up only the candidate targets a specifier * could possibly resolve to, and then confirms each candidate with the real * `importMatchesTarget`. The final match decision is never approximated -- * only the set of pairs the decision is *asked about* shrinks. * * ### The necessary condition, and why it holds * * Write `segs(x)` for the `/`-separated segments of `x`, `tail(x)` for its * last segment. Every branch reachable from `importMatchesTarget(S, f, T)` * (with `S` normalized to `nS`) implies: * * tailKeys(nS) intersects allKeys(T) OR tailKeys(T) intersects allKeys(nS) * * where `allKeys` = every segment plus each dot-part of each segment, and * `tailKeys` = the last segment plus its last dot-part (all lowercased -- * `matchesPHPNamespace` compares case-insensitively, so the keys must too). * Branch by branch: * * - **Exact match** (`nS === T`): tails are equal. ✔ * - **`matchesFile` strategy 1** (`T` occurs in `nS` at `/` boundaries): * `segs(T)` is a contiguous run of `segs(nS)`, so `tail(T)` is a segment of * `nS`. ✔ (right-hand disjunct) * - **strategy 2** (`nS` occurs in `T` at `/` boundaries): `tail(nS)` is a * segment of `T`. ✔ (left-hand disjunct) * - **strategy 3** (the `./`/`../`-stripped `nS` vs `T`, either direction): * containment again; stripping only removes `.`/`..` segments, which are * never a target's segment anyway. ✔ * - **strategy 4, PHP namespaces**: requires the last components to be equal * case-insensitively. ✔ * - **strategy 5, Python dotted modules**: all four sub-strategies compare * `moduleAsPath` (= `nS` with dots -> slashes) against `T` by equality, * prefix, suffix, or `/`-anchored interior, so `tail(moduleAsPath)` -- which * is the last dot-part of `nS`, hence in `tailKeys(nS)` -- is a segment of * `T`. The one exception is `matchesWithSourcePrefix`'s right edge, which * also accepts a `.`; that is why `allKeys` includes dot-parts of each * segment rather than whole segments only. ✔ * - **Rust `mod`/crate-root marker** (`T === nS + '/mod'`): `tail(nS)` is * `T`'s second-to-last segment. ✔ * - **`isUnresolvableWholeModuleImport`** only ever *rejects*, so pruning * before it is safe (this module drops those specifiers at build time, the * same early drop `indexImportEntry` does). * * The keys deliberately over-generate (e.g. `allKeys` carries dot-parts on the * specifier side too, where only whole segments are strictly required). * Over-generating costs a few extra confirmed-by-`importMatchesTarget` * candidates; under-generating would silently lose edges, so every deliberate * looseness here points the same way. `dependent-count-index.test.ts` pins the * property that matters: for a fixture corpus spanning every supported * language, the pruned result is IDENTICAL to the brute-force cross product. * * ## What is deliberately NOT counted * * - **Re-export/barrel transitivity.** `findDependents` merges dependents * reached through a re-export chain (`buildReExportGraph`), which is a * per-target O(files) scan with no build-once/resolve-many split available. * Direct edges still count the barrel as a dependent of the target and its * consumers as dependents of the barrel, so the graph is intact -- only the * collapsed consumer->target shortcut is missing. That makes this an * UNDERCOUNT, never an overcount, which is the correct failure direction for * a signal that only ever promotes a search result. * - **Symbol-level attribution.** File-level only, like * `findDependents(filepath)` with no `symbol`. * - **Self-edges.** A file importing itself (`use crate::OwnType`, a * same-directory barrel) is not a dependent of itself. * * Per #1071's constraint 4: nothing here fabricates a count. A language whose * specifiers genuinely do not resolve keeps a 0, and saying so honestly is * #1072's job, not this module's. */ import type { CodeChunk } from './types.js'; import { buildCSharpTypeReferenceIndex } from './csharp-type-reference-signals.js'; import { buildGoRootPackageIndex } from './go-root-package-signals.js'; import { buildJvmSamePackageIndex } from './jvm-same-package-signals.js'; /** * Lazily-built project-wide indexes for the three non-import recovery tiers. * Built on first use, so a corpus with no zero-dependent C#/Go/JVM file pays * nothing for any of them. * * Exported so `dependency-analyzer.ts`'s `findDependents` can thread the * exact same bag shape through its own `ScanContext` (#1101) -- this is the * one shared home for "which of the three recovery indexes has been built * so far in the current batch", rather than two structurally-identical * interfaces declared separately. The two call sites' caching logic stays * separate (`recoverDependentsForFile` below vs. the three * `enrichWith*Dependents` functions) since their call shapes genuinely * differ; only the bag's TYPE is shared. */ export interface RecoveryIndexes { csharp?: ReturnType; go?: ReturnType; jvm?: ReturnType; } /** * How many DISTINCT other indexed files import each file in `chunks`, keyed on * the raw `chunk.metadata.file` string (see `finalizeCounts` for why raw and * not normalized). * * Only files with at least one dependent appear in the map; read a missing key * as `0`. See the module doc for what is and is not counted, and for why this * is exact with respect to `importMatchesTarget` rather than a second * approximation of it. * * `chunks` must be the FULL project chunk set: both recovery tiers, and every * uniqueness check they rest on, are project-wide properties. * * `options.recoveryTiers: false` suppresses the two non-import recovery tiers, * leaving only import-graph edges. No production caller passes it; it exists so * the contribution of each half can be measured separately, which is how #1071 * established that C# gets essentially ALL of its counts from the type-reference * tier (`using` statements name namespaces, and nothing resolves a namespace to * a file yet — that is #1067's track) while Go/Rust/Python get theirs from * import edges. Without a way to separate them, a future perf regression in one * tier is indistinguishable from a quality change in the other. */ export declare function computeDependentCountsFromChunks(chunks: CodeChunk[], workspaceRoot: string, options?: { recoveryTiers?: boolean; }): Map; /** * Brute-force reference implementation: every (specifier, file) pair through * `importMatchesTarget`, with no candidate pruning at all. Exported for * `dependent-count-index.test.ts`, which asserts that the pruned * `computeDependentCountsFromChunks` agrees with this exactly on a * multi-language fixture corpus -- the property that makes the candidate index * a pruning optimization rather than a third matching dialect. * * Never call this in production: it is precisely the O(files x unique * specifiers) cost the candidate index exists to avoid. */ export declare function computeDependentCountsBruteForce(chunks: CodeChunk[], workspaceRoot: string): Map; //# sourceMappingURL=dependent-count-index.d.ts.map