The foundation
Why graph beats text search
Most AI coding agents fall back on text: grep and ripgrep for exact matches, vector embeddings for fuzzy ones. But source code is a deterministic graph, not a bag of words, so pattern matching, exact or fuzzy, can only ever approximate the structure that actually connects your code.
| Dimension | Text search (grep/ripgrep + vector RAG) | Travsr graph (BFS → PPR → PCST) |
|---|---|---|
| Structural accuracy | Approximate: regex matches surface text and embeddings match surface meaning, neither reads the call graph, type graph, or import graph underneath | Exact: edges are real call graph, type graph, and import graph edges |
| Symbol identity | Name-based: a search for charge hits every unrelated charge() in scope, and misses the real one the moment it's renamed | Stable: Kythe VNames identify a symbol consistently across renames, re-indexing, repos, and languages |
| Multi-hop reasoning | Manual: the agent chains searches by guessing the next name to look for, one hop at a time, and stalls at interfaces or dynamic dispatch | Native: BFS and PPR walk real edges across files and packages in a single traversal, no guessing |
| Round-trip cost | Higher: every dead-end search is its own tool call, and each one burns a turn re-reading files just to check relevance | Fewer tokens: PCST returns the smallest connected subgraph that answers the query in one call |
| Hallucination risk | Higher: when a search comes back empty or partial, the model fills the gap from training data instead of your actual codebase | Only real edges: the graph is built from relationships found in your code, not inferred |
| Freshness | Split: grep reads live files but stays blind to structure; embeddings carry structure but are often rebuilt on a batch schedule that lags behind HEAD | Both: a git post-commit hook reindexes changed files on every commit, so structure stays fresh too |
| Local-first | Mixed: grep stays on your machine, but embedding setups often send code to an API and store vectors in a cloud DB | Yes: the graph lives on your machine |
See the difference
One question, two answers
“Who calls PaymentService.charge?” - checkout.ts: an unrelated local charge() helper, name matches, symbol doesn't
- billing.test.ts: a mock named charge that asserts nothing about real callers
- docs/payments.md: prose that mentions “charge” in passing
Same string, not the same symbol. The real call site, reached only through PaymentGateway.pay()'s dispatch, never gets matched because the text doesn't match.
- api/orders.ts → OrderController.submit
- jobs/retry.ts → RetryWorker.run
- billing/refund.ts → Refund.reverse
Every real call site across every package, walked from ref/call edges. Nothing invented.
How the graph is built
Tree-sitter: structural AST
Every file is parsed into an AST without a compilation step. Tree-sitter grammars produce import edges, class/function declarations, and call expressions as graph nodes and edges.
LSIF & SCIP: semantic edges
Language indexers resolve precise type references, call targets, and
data-flow edges that Tree-sitter alone can't see: rust-analyzer
for Rust, an LSIF emitter for TypeScript/JavaScript, and scip-*
indexers for Python, Go, Java, and more, added on demand with
travsr lang install.
Kythe VNames: stable identity
Every node gets a globally unique Kythe VName, so the same symbol keeps a consistent identity across re-indexing, repositories, and languages. Cross-repo edges are first-class: your app can reference your library by VName.
BFS → PPR → PCST retrieval
Queries are answered by graph traversal, not vector similarity. BFS finds direct neighbours. Personalized PageRank scores importance. Prize-Collecting Steiner Tree extracts the minimum connected subgraph that fits within a token budget.
Cross-encoder: honest confidence
For natural-language queries, a small local cross-encoder
(ms-marco-MiniLM, ~45 MB, CPU-only) re-scores the candidates the
graph already found and reports an honest confidence, or abstains,
instead of guessing. It only ranks what the graph surfaced, it never
decides which edges exist. The optional travsr-embed
sidecar adds recall on top; it doesn't set relevance.
Twelve tools, not a prompt
The graph is exposed over the Model Context Protocol, so any MCP client
(Claude, Copilot, Cursor) traverses it directly over stdio or SSE.
No re-prompting, no guessing: the client reads the graph directly.
travsr init detects the tool you have installed and wires
the server plus an always-on rules file automatically, no manual config.
get_dependenciesget_callersget_blast_radiussearch_symbolget_repo_mapget_execution_pathget_contextget_graph_statsget_graph_jsonget_snippetsget_lang_statusrepo_languages
Always fresh, by construction
A git post-commit hook re-indexes only what changed via a
SHA-256 delta. There's no nightly embedding job to drift behind HEAD, so
the context an agent reads aims to stay in step with the code you just
wrote.