/** * Graph centrality over the symbol wiring graph. * * Two modes share one implementation: * * - **Global** — computed once per index run with a uniform restart * distribution, persisted to `symbol_rank` / `file_rank`. Answers "what is * architecturally central in this repo", and drives the repo map, the * CodeMap node sizing and the atlas projection. * - **Personalised** — computed per query with the lexical (BM25) hit scores * as the restart distribution. Answers "what is central *to this query*". * Lexical search proposes the seeds; the graph decides the ordering. * * The walk runs over an **undirected** view of the graph: a caller and its * callee are mutually relevant when you are trying to understand either one, * and a directed walk starves leaf utilities that everything depends on. * Directed in/out degrees are still reported separately, because they are * what a human reads as "blast radius" vs "coupling". * * Only refs that resolved to a real symbol id participate. At the time of * writing roughly a third of this repo's refs stay unresolved (stdlib and * third-party targets that were never indexed); including them would mean * walking to nodes that carry no code. * * Edge multiplicity is deliberately preserved. Five calls from A to B produce * five edges, so B receives five shares of A's mass: repeated coupling is * stronger coupling. Self-references are dropped — recursion should not make * a symbol important on its own account. * * ## Why edges carry a confidence weight * * The index resolves a ref by matching `to_name` to the lowest symbol id in * the same language family — it is not file- or import-aware, which * `findIncomingCallsByName` already documents as an ambiguity source. For a * per-query answer that is a caveat; for a global ranking it is fatal. On this * repo roughly half of all resolved refs point at a name that several symbols * declare, and the worst offenders are test globals and single-letter locals: * every `it(...)` in the suite lands on whichever `it` happens to hold the * lowest id, handing that one symbol thousands of incoming edges it never had. * * So an edge into a name that `n` symbols declare carries weight `1 / n`: the * resolver guessed one of `n` equally plausible targets, and the walk spreads * exactly that much belief. Unique names — the half we can actually trust — * keep full weight. * * Homonym counting alone is not enough, because the worst misresolutions are * to names that really are unique. Every `it(...)` in the test suite resolves * to the one `const it: Catalog` in the desktop app's i18n file — the Italian * locale — handing a translation table 5,500 incoming edges. Nothing declares * a competing `it`, so no homonym penalty applies. * * The second weight is therefore visibility, in three tiers. An edge is fully * trusted when the two symbols share a file, or when the source file actually * imports the target's file — the module resolver already recorded those pairs * in `refs.to_file`. Otherwise the question is whether we can believe the * absence of that import: * * - The source file has resolved imports, and the target's file is not among * them. The resolver worked here and still did not connect these two files, * so the reference almost certainly is not to this symbol * ({@link CONTRADICTED_VISIBILITY_WEIGHT}). * - The source file has no resolved imports at all. Resolution failed * wholesale for it, so its silence is not evidence either way, and the edge * keeps a reduced but meaningful weight * ({@link UNVERIFIED_VISIBILITY_WEIGHT}). * * These stay penalties rather than filters: module resolution only resolves * about three fifths of this repo's import specifiers, and dropping the rest * outright would gut the graph. * * Both of these are ranking-quality fixes, not resolver fixes; making ref * resolution import-aware would remove the need for either. */ /** Ref shape as returned by `IndexStore.getAllResolvedRefs()`. */ export interface ResolvedRefEdge { fromId: number; toId: number; callType: string; } /** * Compressed sparse row adjacency over a dense node numbering. * * `neighbours[offsets[u] .. offsets[u + 1]]` are the dense indices adjacent to * dense node `u`. `ids[u]` maps back to the original symbol id. */ export interface WiringGraph { /** Number of nodes. */ size: number; /** Original symbol id for each dense index. */ ids: Int32Array; /** Dense index for each original symbol id. */ index: Map; /** CSR row offsets, length `size + 1`. */ offsets: Int32Array; /** CSR adjacency entries, length `offsets[size]`. */ neighbours: Int32Array; /** * Confidence per adjacency entry, parallel to `neighbours`, in `(0, 1]`. * * This attenuates rather than redistributes: an edge with confidence `c` * carries `c` of the mass it would otherwise carry, and the remaining * `1 - c` returns to the restart distribution as belief the walk declined * to place. Normalising these away — dividing each node's split by the sum * of its weights — would make them inert, because a node with a single * low-confidence edge would still hand over all of its mass. */ weights: Float64Array; /** Directed in-degree per dense node (how many symbols reference it). */ inDegree: Int32Array; /** Directed out-degree per dense node (how many symbols it references). */ outDegree: Int32Array; } export interface PageRankOptions { /** * Restart (teleport) probability — the share of mass that returns to the * restart distribution each iteration. Higher keeps the walk closer to its * seeds. 0.25 is the value Graft settled on for code graphs; the classic * web-PageRank 0.15 wanders too far for personalised queries. */ restart?: number; /** Power iterations. 25 converges comfortably at this graph size. */ iterations?: number; /** * Seed weights by **dense index**, for a personalised walk. Weights need not * be normalised. Omitted (or empty) means a uniform restart distribution. */ seeds?: ReadonlyMap | undefined; } export declare const DEFAULT_RESTART = 0.25; export declare const DEFAULT_ITERATIONS = 25; /** * Weight for a cross-file edge from a file whose imports never resolved. We * cannot corroborate the edge, but we cannot contradict it either. */ export declare const UNVERIFIED_VISIBILITY_WEIGHT = 0.25; /** * Weight for a cross-file edge from a file whose imports DID resolve, to a * file it does not import. Near-zero rather than zero so a single resolver * miss degrades one edge instead of erasing it. */ export declare const CONTRADICTED_VISIBILITY_WEIGHT = 0.02; /** * Build the undirected CSR wiring graph from the resolved ref list. * * Two passes over the edge list (count, then fill) so the adjacency lands in * flat typed arrays with no intermediate per-node arrays — at 180k edges the * array-of-arrays shape costs more in allocation than the walk itself. */ export interface WiringGraphOptions { /** * How many symbols declare the name each target symbol carries, keyed by * target symbol id. Absent ids are treated as unambiguous (weight 1). */ candidates?: ReadonlyMap | undefined; /** Declaring file per symbol id, used for the visibility check. */ fileOf?: ReadonlyMap | undefined; /** Files each source file imports, as resolved by the module resolver. */ importsOf?: ReadonlyMap> | undefined; /** * True when the target is visible to the source without a file-level import * — the same Go package, Java package or C# namespace directory, or a file * of a Go package the source imports (the resolver records ONE * representative file per imported package). Those edges are ordinary * code, not misresolutions, and must not take the contradicted weight. */ implicitlyVisible?: ((sourceFile: string, targetFile: string, imports: ReadonlySet | undefined) => boolean) | undefined; } export declare function buildWiringGraph(refs: readonly ResolvedRefEdge[], options?: WiringGraphOptions): WiringGraph; /** * Power-iterate PageRank over `graph`, returning scores normalised so the * highest is exactly 1.0. Nodes the walk never reaches score 0. * * Mass is conserved exactly: every node hands `restart` of its mass back to * the restart distribution, spreads the rest over its neighbours, and a node * with no neighbours hands everything back. Low-confidence edges return their * unspent share to the same pool. Without it the total would leak downwards * and the final normalisation would hide the leak. */ export declare function pageRank(graph: WiringGraph, options?: PageRankOptions): Float64Array; /** One persisted `symbol_rank` row. */ export interface SymbolRankRow { symbolId: number; rank: number; inDeg: number; outDeg: number; } /** One persisted `file_rank` row. */ export interface FileRankRow { file: string; rank: number; inDeg: number; outDeg: number; } /** Materialise the score vector as persistable symbol rows. */ export declare function toSymbolRankRows(graph: WiringGraph, scores: Float64Array): SymbolRankRow[]; /** * Roll symbol scores up to files. * * A file's rank is the **sum** of its symbols' ranks, not the mean: a module * that exports twenty things everyone uses is more central than one that * exports a single equally-used thing, and averaging erases exactly that. * Degrees roll up the same way. The result is re-normalised to max 1.0 so * file and symbol ranks share a scale. */ export declare function aggregateFileRank(rows: readonly SymbolRankRow[], fileOf: ReadonlyMap): FileRankRow[]; //# sourceMappingURL=graph-rank.d.ts.map