The Algorithms
This is the heart of travsr, and the heart of its first principle: “algorithms first, LLM last.” When you ask a question, classic graph algorithms find the answer. The AI only rephrases your question going in and formats the answer coming out. Here are the five algorithms that actually ship, each animated, each with the real numbers from the code.
① Personalized PageRank (PPR): “what’s relevant to this?”
This is the workhorse. Same math Google used to rank web pages, but “personalized” to a starting point. The question it answers: “If I start at this function and wander the call-graph, preferring strong roads (calls) over weak ones (imports), which other functions do I keep ending up at?” Those are the most relevant ones.
Drop a thousand walkers on charge(). At every step they either follow an outgoing
road (bigger roads more likely) or, with a 15% chance, teleport back to the start.
Count where the crowd piles up. That pile-up is the relevance score.
Pick a seed and step through. Brighter = higher PageRank score = more relevant.
The real knobs (from the code)
ALPHA (damping) | 0.85 |
EPSILON (stop when change <) | 1e-6 |
MAX_ITERATIONS | 50 |
All three are overridable at runtime via
TRAVSR_PPR_ALPHA / TRAVSR_PPR_EPSILON / TRAVSR_PPR_MAX_ITER.
One iteration, in words
For every node, its new score =
85% of the score flowing in along edges (split by each road’s weight)
+ 15% teleported back to the seed. Repeat until the scores stop moving
(≈ 15–30 rounds). A recent upgrade, ppr_weighted, lets you start from
several seeds, each with its own confidence, that’s how semantic search plugs in (Chapter 4).
② 0/1 Knapsack: “fit the best answer in the token budget”
PageRank gives you a ranked pile of relevant functions. But an AI has a limited “context window”, you can only send it so many words (tokens). Which subset do you send? The one with the most total relevance that still fits. That’s the classic knapsack problem.
You’re packing a suitcase with a strict weight limit. Each item has a value (PageRank score) and a weight (how many tokens it costs). Maximize value without going over. travsr literally solves this to build the AI’s context.
Drag the budget. Green = packed (sent to the AI). Grey = left out. The solver maximizes total relevance under the limit.
The real details (from the code)
- Token cost of a node ≈
(signature + kind + path) length ÷ 4characters-per-token. - Exact 2-D dynamic programming when the table (
items × budget) ≤ 500,000 cells; otherwise a fast greedy value/cost fallback. - Hard cap
MAX_CONTEXT_BUDGET = 32,000tokens (Claude’s practical limit).
③ PCST: “connect these two functions cheaply”
The get_execution_path(source, sink) tool answers: “how does
data/control flow get from A to B?” travsr finds the cheapest chain of edges linking them.
It’s a practical approximation of a “Prize-Collecting Steiner Tree” (PCST).
Cheapest route between two landmarks. Strong roads (direct calls) are “cheap” to travel; weak roads cost more. The route it returns is the most likely real execution path.
Pick a start and end. The highlighted chain is the lowest-cost path, where cost = 1 − edge weight.
The real details (from the code)
- Builds a bidirectional BFS bubble around source & sink (depth ≤ 5, ≤ 2000 nodes).
- Runs Dijkstra over it with edge cost =
1.0 − ppr_weight(strong calls are cheapest). - If no path is found (endpoint unreachable within the bubble), it safely falls back to a depth-3 BFS. An earlier wall-clock timeout was deliberately removed: a post-hoc check can't interrupt already-completed work, and it made results nondeterministic.
④ K-core: “find the structural backbone”
Some code is peripheral (a one-off helper); some is central (everything leans on it). K-core decomposition assigns every node a “shell number”: how deep into the densely-connected core it sits. travsr uses this to boost central code in results.
Peel an onion from the outside in. Loosely-connected nodes peel off first (low shell). The tightly-interwoven center survives longest (high shell), that’s your downtown.
Repeatedly remove the least-connected nodes. The round a node is removed in = its shell number.
The real details (from the code)
- Batagelj–Zaveršnik bucket-peeling, runs in O(V + E) (linear, very fast).
- Two phases: peel to get raw shells, then propagate a package’s shell down to the symbols it contains, so a function in a central module inherits that centrality.
- Recomputed after every index so it’s always current.
⑤ BM25: “rank by keyword match”
Before any graph walk, travsr has to find a good starting node from your words. BM25 is the classic search-engine ranking that librarians and Elasticsearch use: it scores how well a query’s words match each symbol’s text, rewarding rare words and not over-rewarding long documents.
A librarian ranking books for your search. A book that mentions a rare word you asked for ranks higher than one full of common words. Length is accounted for so thick books don’t win by bulk.
Type words found in the function names. Bars show BM25 relevance scores. Tokenizer splits camelCase & snake_case, like the real one.
The real details (from the code)
- Okapi BM25 with
K1 = 1.2,B = 0.75(textbook defaults). - Robertson–Spärck-Jones IDF with +1 smoothing so scores stay positive.
- Same tokenizer as the search index: splits
camelCase&snake_case, drops tokens shorter than 3 chars.
How they combine: the get_context pipeline
When an AI calls get_context("how are payments charged?", budget=2000),
all five cooperate, alongside one neural step, a cross-encoder reranker (travsr-rerank,
RFC-021) that judges relevance directly rather than by graph position:
Find & fuse seeds (RRF)
Three candidate lists, exact/anchor matches, lexical full-text (BM25-style FTS), and, if enabled, semantic KNN, are merged with Reciprocal-Rank Fusion (constant k = 60). A node that ranks high in several lists wins. If nothing scores well enough, travsr abstains rather than return noise.
Rerank the seeds
The cross-encoder scores each fused seed against the query directly. Its score feeds the seed's PPR personalization weight (RFC-022), so it shapes where PageRank starts rather than just re-sorting a finished list. Skipped entirely if the model is absent.
Walk the graph
Weighted PageRank (ppr_weighted) spreads relevance out from those seeds along the call edges, higher-confidence seeds pull harder.
Boost the backbone
K-core shells nudge structurally-central code up the ranking.
Pack the budget
Knapsack selects the most-relevant subset that fits the token budget.
Return clean context
The AI gets exact file:line references, real structure, no hallucinated links.