What gets ranked—and what the rank means
For ripwire, the hard question begins after a repository has become a set of symbols and references: which definition does a reference mean, and which kind of importance should a coding agent see first? This chapter follows that decision from approximate name resolution through graph ranking and task retrieval. The goal is practical interpretation: read a high-ranked symbol as structural importance or task relevance according to the surface that produced it, notice when the resolver could not decide, and know when to ask for stronger evidence.
The distinction matters because “rank” sounds more precise than it is. A function can be central to the architecture but irrelevant to a particular change. A class whose name matches an issue can be the right lexical entry point while having little call-graph influence. A resolver can connect one call to several plausible definitions and still produce a useful structural map, provided it does not conceal the ambiguity. Ripwire’s design keeps these cases separate rather than forcing them into one universal score.
When names stop being enough
A source file often tells us that a call named run exists. It does not always tell a build-free tool which of several run definitions will execute. A compiler can use types, imports, overload rules, build configuration, and language semantics to narrow that choice. Ripwire begins with less information. Its baseline resolver therefore uses a fixed locality ladder rather than claiming full semantic resolution.
The ladder first looks for a compatible definition in the same file. If none is available, it looks in the same directory. If the remaining compatible definition is unique across the indexed tree, it can use that global candidate. If several definitions remain at the global tier, the resolver drops the edge rather than inventing a tie-break. This order captures a useful fact about many repositories—nearby definitions are often more likely to be intended—without confusing proximity with proof.
The resolver’s treatment of a tie is equally important. When a chosen tier contains several candidates, ripwire can emit an edge to each survivor with the weight divided among them. It does not quietly choose the first symbol encountered, the shortest name, or the file with the most convenient path. The symbol carries an ambiguity count, and the edge can carry a split provenance marker. The graph remains usable for structural ranking, but the output tells the agent that a source read is needed if the identity of the callee matters.
This is a deliberate trade. Dropping every uncertain relationship would make the graph too sparse to describe how a repository is connected. Picking one candidate would make the graph look cleaner while manufacturing confidence. Splitting preserves the possibility that the relationship contributes to importance, while preventing one arbitrary choice from masquerading as a fact. The result is not a sound call graph; it is a ranked graph with a visible uncertainty boundary.
Ripwire adds more precise narrowing rules, but they all obey the same constraint: a refinement may remove candidates only when the source supplies evidence for doing so. A qualified call such as a scoped C++ name can be matched against a canonical scope-and-name table. A call through this or self can be narrowed to the enclosing class when that class actually defines the method. A local variable with one unambiguous type binding can narrow Foo x; x.m() toward Foo::m. A field receiver can use the field’s declared type when the class-field relationship is itself unambiguous. A resolved include or import can narrow an ambiguous name to the one included file that contains a candidate.
The direction of these rules is what makes them safe. They do not create a definition that the name-based candidate set did not already contain. If a type binding conflicts, an import resolves to more than one file, a path cannot be resolved uniquely, or an inheritance cone would remove every candidate, the refinement gives up and the broader ladder remains in charge. A plausible narrowing that points to the wrong class is more dangerous than an honest split, because it supplies a clean edge at exactly the point where the agent is most likely to stop checking.
Some language relationships can be made more precise through explicit provenance. Cross-language foreign-function bindings are labeled as bindings rather than ordinary name matches. JavaScript and TypeScript named imports can be pinned to an indexed module export when the module and export facts are unambiguous. The edge then says that an import relation supplied the evidence. These labels do not turn a syntactic index into a compiler; they tell the reader which mechanism produced a particular relationship.
A SCIP index provides the clearest escalation path. When an external compiler-grade index covers a call site, its target set replaces the name-based candidates for that site, and the resulting edge is labeled prov="scip". A missing or empty index is refused rather than silently treated as if precision had been supplied. A corrupt index produces a warning and falls back to name-based analysis. That behavior preserves an important distinction: “precision was not requested,” “precision was unavailable,” and “precision was requested but could not be trusted” are different operational states.
The same principle governs unresolved and external names. A call to a standard-library name with no definition in the repository is not automatically evidence that ripwire missed an internal function. A name defined in the repository but filtered out by language compatibility is a different kind of unresolved case. A dynamic call whose target is determined at runtime may produce no edge at all. The graph records selected categories of these absences so a zero does not have to carry several contradictory meanings.
The agent should consequently ask two separate questions of a call edge: “How was this relationship established?” and “How much does this relationship matter to the ranking?” The first is about resolution and provenance. The second is about the ranker. Confusing them is how a useful approximation becomes a false authority.
Importance and relevance are different rankings
Once the graph has edges, ripwire can rank its symbols by structural importance. Its default graph uses an in-edge compressed sparse row representation: each target stores the sources that point to it. That orientation matches the question PageRank asks. A symbol becomes important when other important symbols call it, so the ranking gathers influence along incoming relationships rather than treating every file as an undifferentiated document.
The edge weights are not simple call counts. For each source-to-target pair, ripwire combines the mean confidence of the references with the square root of the number of references, then caps the result. The square root makes repeated calls matter without allowing a hot loop to dominate solely through call-site multiplicity. Self-loops are removed because a recursive edge can otherwise act as a rank sink and inflate the symbol that points only to itself.
Personalized PageRank then repeatedly redistributes rank through those weighted edges. Its teleport vector supplies the prior that keeps probability mass from disappearing at dangling symbols—leaf functions, data structures, or interfaces with no outgoing calls—and it is also the only place where personalization enters. A changed-file map or churn-weighted view biases that vector; it does not multiply the final ranks after the fact. The default damping factor is 0.85, and the rank calculation returns its iteration count and convergence state along with the vector.
This produces a meaningful structural question: which symbols are load-bearing in the indexed call graph? It does not necessarily answer “where is the implementation of the task I just described?” Nor does it answer “which files tend to change together?” The repository records a particularly clear negative result for the last question: on a historical co-change evaluation, PageRank reached only 3.8% recall at five while lexical body retrieval reached 40.3%. Fusing the structural rank into the lexical rank made the result worse. Relatedness and importance are different signals, so ripwire keeps different machinery for them.
Task retrieval uses lexical evidence. Ripwire splits identifiers into subtokens so a query about “split chunks plugin” can match a compound name even when the query does not reproduce the exact spelling. Its BM25 documents include name subtokens, documentation comments, body text, and selected callee names. A rare term that appears in the implementation or its explanation can therefore outweigh a common name shared across the repository.
There are two principal retrieval lanes. The name-exact lane is suited to a query that names a symbol, such as rankGraphTeleport; whole-name matches avoid being diluted by vocabulary from a large body. The subtoken-plus-body lane is suited to a conceptual request such as “where is the cache staleness check?” because the answer may use different words in its name and explain the mechanism in a comment or implementation. Neither lane is universally superior. For name-shaped queries, the repository reports a recall-at-one increase from 61.1% without routing to 91.3% with routing on its source corpus. For documentation-phrase queries, forcing everything into the name-exact lane collapses mean reciprocal rank to about 0.016, whereas the conceptual lane reaches about 0.967 in the same measurement.
The router is therefore confidence-gated rather than merely keyword-triggered. Identifier shape is strong evidence that a query is naming something. A query whose content words all happen to be symbol names is not automatically a name query: common names can satisfy that test while pointing nowhere useful. Ripwire checks how many definitions carry the matched names and whether the terms are specific enough to distinguish a target. A query about split chunks, for example, may contain real symbol names while still being better served as a conceptual search if those names are too common. The output records which lane ran and why, so a reader can understand a ranking without reconstructing the router.
Literal mentions supply another kind of evidence. If the task text names a path, module, or Type.method, ripwire can lift that artifact toward the top of the result. This is useful for issue descriptions that paste the location directly, but it is a score adjustment rather than an absolute rank guarantee. A flat head can still leave the mentioned item below the first row, and a query that mentions no indexed artifact is unchanged by the feature.
Finally, adaptive selection can cut a result at its largest relative relevance gap instead of padding every query to a fixed number of rows. A sharp query may return only a small set; a broad query may have no meaningful cliff and remain broad. The confidence marker attached to a task result describes this separation in the ranking. It says whether the returned head is sharply distinguished, not whether the tool has correctly understood the user’s intent.
The sequence has two paths because ripwire is answering two different questions. A source reference first passes through candidate resolution, where it may be pinned, split, or declined. A repository task passes through lexical retrieval, where query shape determines the ranker. Both paths eventually reach the same serving layer, but their ranks should not be compared as if they were measurements on one scale.
That is the interpretive discipline to carry forward. A structurally important symbol is a good place to understand the architecture. A lexically relevant symbol is a good place to begin a task-specific search. An ambiguous or weakly grounded result is a prompt to gather more evidence, not an invitation to choose the most attractive row. Once those distinctions are explicit, the practical question becomes how much of the selected context an agent should actually receive—and how a response can remain useful when a budget cuts it short.