Skip to main content

Architecture

This page is for engineers evaluating Travsr or contributing to it. It explains how every component fits together.

Overview

git commit
post-commit hook
travsr-daemon
scheduler
file watcher
changed files
Tree-sitter
structural AST
+
LSIF compiler
semantic edges
nodes + edges
travsr-store
SQLite WAL.travsr/graph.db
Kythe VNamesnode identity
graph query
travsr-retrieval
BFSget_dependencies · get_callers
PPRget_context ranking
PCSTget_execution_path
token budget
0-1 Knapsack
MCP tool response
travsr-mcp
stdiolocal daemon
SSETravsr Cloud
Local MCP client
Claude Desktop · Cursor · any IDE agent
Remote MCP client
via Travsr Cloud

Crate structure

Travsr is a Rust workspace. The dependency graph is strictly acyclic:

travsr-core Foundation. Node, Edge, EdgeKind, VName (Kythe address),
NodeId (BLAKE3), and ppr_weight(). No internal deps.
travsr-error Shared error taxonomy (StoreError, IndexError,
TravsrError). No internal deps.
travsr-ipc Control-plane transport (Unix socket / Windows named
pipe) between the CLI and the running daemon. No
internal deps.
travsr-config Layered, typed configuration. Resolution order, highest
precedence first: CLI flag, environment variable,
per-repo .travsr/config.toml, global
~/.travsr/config.toml, built-in default. A pure
resolver: callers do the I/O at the edges and pass
values in, so the same inputs always produce the same
result. No internal deps.
travsr-rerank In-process cross-encoder relevance arbiter. Pure ONNX
inference over (query, candidate) pairs, batched across
rayon's thread pool. No internal deps.

travsr-plugin-protocol Depends on: core, error
Plugin trait, wire message types, length-prefixed frame
codec for sidecars (language and embed).
travsr-plugin-sdk Depends on: plugin-protocol, core
The harness a sidecar binary links against to implement
the protocol.

travsr-analysis Depends on: core
Phase A. Tree-sitter parsers for every language, the AST
"skeleton" builder used for embedding text, snippet
extraction, and data-format parsing (JSON, YAML, TOML,
XML).
travsr-store Depends on: core, error
SQLite (WAL) backend: nodes/edges/files/meta tables, FTS5
search, migrations, RBAC columns.

travsr-indexer Depends on: core, analysis, error
Orchestrates indexing: Phase A parsing, Phase B LSIF/SCIP
runners, the SCIP unifier, the sandbox, and file hashing.
travsr-retrieval Depends on: core, error, store
Algorithms: BFS, PPR (ppr / ppr_weighted), PCST, k-core,
BM25, 0-1 knapsack, and RBAC edge filters.
travsr-plugin-host Depends on: plugin-protocol, core, error, indexer,
analysis, config
Owns the trust boundary. Hosts native and sandboxed
Phase B language analyzers plus the embedding
sidecar/supervisor.

travsr-mcp Depends on: core, error, analysis, retrieval, store,
plugin-host, config, rerank
The Model Context Protocol server (stdio + SSE). The only
external interface.

travsr-daemon Depends on: core, ipc, mcp, indexer, retrieval, store,
plugin-host, analysis
Long-running orchestrator: git hook, file watcher,
incremental reindex, Phase B scheduler, query cache.

travsr-cli Depends on: core, ipc, indexer, daemon, mcp, store,
retrieval, plugin-sdk, plugin-host, config
The travsr binary: init, connect, ask, graph, mcp, lang,
embed, rerank, synonym, config, serve, and more.

The edges above are the real ones read from each crate's Cargo.toml, not an idealized summary. The "no cycles" rule still holds; the web is just richer than a single spine (for example travsr-mcp uses store, analysis, and plugin-host directly, not only retrieval).

Sidecar packages live under packages/: travsr-npm (the @travsr.com/travsr binary wrapper published to npm), travsr-vscode (the VS Code extension), travsr-lsif-ts (the built-in TypeScript/JavaScript LSIF emitter), and travsr-lsif-py (the Python LSIF emitter).

Indexing pipeline

1. Tree-sitter (structural layer)

Tree-sitter parses every source file using language grammars into an AST without a compilation step. Travsr extracts:

  • Import edges: import X from Y / use crate::X / from X import Y
  • Definition nodes: functions, classes, structs, enums, traits, interfaces
  • Call expression nodes: unresolved call sites (resolved by LSIF)
  • Inheritance edges: extends, implements, struct ... : Trait

Tree-sitter handles all languages at the structural level. LSIF provides deeper semantic information for supported compilers.

2. Semantic enrichment layer (Phase B)

Tree-sitter is Phase A. A second per-language pass, Phase B, resolves call targets, type references, and cross-module edges, then merges them into the same graph. Phase B is hosted by travsr-plugin-host: TypeScript, JavaScript, Rust, and Python run an in-process native analyzer, while the rest run an external LSIF/SCIP tool in a sandboxed subprocess installed via travsr lang install.

Native Phase B (in-process, zero install, runs automatically):

LanguageOptional external upgradeEdges added
TypeScript / JavaScripttravsr-lsif-ts (LSIF)Resolved call targets, type references, DFG
Rustrust-analyzer (LSIF)Resolved call targets, trait implementations
Pythontravsr-lsif-py (LSIF)Resolved call targets, type inference

These four languages have a native Phase B implementation compiled into the daemon, so call edges are indexed with nothing installed. The external tool in the middle column is an upgrade, not a requirement: travsr lang list reports the language as active either way. Rust's upgrade is fetched by travsr lang install rust, which uses rustup when it is present and otherwise downloads a pinned rust-analyzer directly.

Installable, Standard sandbox (no network, no dependency downloads):

LanguageUnderlying tool
Goscip-go
Rubyscip-ruby
PHPscip-php
Swifttravsr-swift-index-emitter
Objective-C (macOS)travsr-lang-objectivec

Installable, Native IPC sandbox (no network, but needs POSIX message queues and shared memory for the analyzer's parallel workers, so macOS applies resource caps instead of sandbox-exec):

LanguageUnderlying tool
C / C++scip-clang
Darttravsr-dart-index-emitter

Installable, Elevated sandbox (build tool fetches dependencies over an approved host allowlist):

LanguageUnderlying tool
Javascip-java
Kotlinkotlin-language-server
C#scip-dotnet
Scalasbt

Notes:

  • Phase B is gated per language against ~/.travsr/lang.toml; a language present in the repo but not registered stays at the Tree-sitter structural graph.
  • If an external tool is absent from PATH, the pass is skipped and Travsr falls back to the Phase A structural graph rather than failing the index.
  • Java and Scala cannot run inside the Windows isolation layer, because their build tools fork workers that fail in it. On Windows they are skipped unless you grant permission with travsr lang allow-unsandboxed; on macOS and Linux they are unaffected.

Semantic edges go into the same graph as Tree-sitter edges. Nodes are deduplicated by VName.

3. Kythe VNames: node identity

Every node is identified by a Kythe VName:

{
"corpus": "github.com/org/repo",
"root": "",
"path": "src/auth/jwt.ts",
"language": "typescript",
"signature": "#sign(string,Claims):string"
}

VNames are content-addressed and stable across renames (if the signature is preserved). Cross-repo edges reference VNames in other corpora.

Storage

SQLite + WAL

The graph is stored in .travsr/graph.db (SQLite). WAL mode enables concurrent reads during reindex.

SQLite is the only backend. Kùzu was once planned as a separate production engine and shipped for a while behind a --features kuzu flag that was never built by default; it was removed entirely in July 2026, along with the travsr migrate command and the one-way migration path that went with it. SQLite + WAL has been sufficient for every target workload. RocksDB remains a possible future backend for hyperscale, but nothing is built against it today, and adopting it would mean designing a fresh migration path.

The v1 schema, which every later migration builds on:

CREATE TABLE nodes (
id INTEGER PRIMARY KEY,
corpus TEXT NOT NULL, -- the four VName fields below plus corpus
root TEXT NOT NULL, -- form the node's Kythe address
path TEXT NOT NULL,
language TEXT NOT NULL,
signature TEXT NOT NULL,
kind TEXT NOT NULL -- 'function' | 'class' | 'module' | ...
);

-- The VName is the real identity; the INTEGER id is a compact surrogate that
-- keeps the edge table narrow and its indexes cache-friendly.
CREATE UNIQUE INDEX idx_nodes_vname
ON nodes(corpus, root, path, language, signature);

CREATE TABLE edges (
src INTEGER NOT NULL,
dst INTEGER NOT NULL,
kind TEXT NOT NULL, -- 'calls' | 'imports' | 'inherits' | 'types' | ...
PRIMARY KEY (src, dst, kind)
);

CREATE INDEX idx_edges_src_kind ON edges(src, kind);
CREATE INDEX idx_edges_dst_kind ON edges(dst, kind);

CREATE TABLE files (
path TEXT PRIMARY KEY,
sha256 TEXT NOT NULL,
last_indexed_at INTEGER NOT NULL
);

Migrations run forward from there through a backend-agnostic MigrationRunner. Later versions add edge provenance and confidence, per-language package metadata, RBAC columns, an FTS5 index over identifiers with a synonym layer, a vector table for embeddings, Phase B symbol unification, cached k-core shells, node tombstones, and a split lexical index for signature and path terms. travsr status and the get_index_status MCP tool report the schema version the current graph is on.

Retrieval algorithms

BFS

Breadth-first search from a seed set of nodes to depth 3. Fast, simple, correct for direct neighbours. Used by get_dependencies, get_callers, and get_blast_radius.

Personalized PageRank (PPR)

PPR scores all nodes in the graph relative to a seed set. High-scored nodes are strongly connected to the seed via many short paths. Used by get_context to rank which parts of the graph are most relevant to a query.

Prize-Collecting Steiner Tree (PCST)

PCST finds the minimum connected subgraph that includes a set of terminal nodes (prize nodes) while minimising the total edge weight. Used by get_execution_path and get_context to extract the token-budget-optimal subgraph. Implementation follows the GW algorithm.

0-1 Knapsack (token budget)

After PPR and PCST produce a candidate subgraph, the knapsack solver selects which nodes to include in the final MCP response given a token budget. Each node has a weight (token count) and a value (PPR score). The solver maximises total value subject to the budget constraint.

k-core decomposition

Assigns every node a shell number: the largest k for which the node survives in a subgraph where every node has at least k neighbours. High-shell nodes are the structural backbone of a codebase, which is what lets retrieval recover the "buried middle" that neither the entry points nor the leaves account for. Shells are computed once and cached in the graph rather than recomputed per query.

BM25 lexical ranking

Ranks symbols lexically against the query text. It tokenises through the same identifier tokeniser the FTS index uses, so camelCase and snake_case split identically on both sides and a query written either way still matches.

Cross-encoder reranking

An optional last pass. Where the algorithms above score structure and lexical overlap, the cross-encoder in travsr-rerank scores (query, candidate) pairs directly and reorders the shortlist, reporting a confidence rather than guessing. It is a relevance arbiter over candidates the graph already produced, never a source of candidates itself, so it cannot invent a node or an edge. Install it with travsr rerank install.

RBAC filtering

Corpus-scoped access control is applied during traversal, not as a filter over the results. Post-filtering would leak the existence of restricted nodes through response timing, so a traversal that is not permitted to see a node never expands it in the first place.

MCP transports

stdio (local)

The default transport. The MCP server reads JSON-RPC from stdin and writes to stdout. Claude Desktop spawns travsr mcp --stdio as a subprocess.

SSE (cloud)

For Travsr Cloud, the MCP server runs as an HTTP server with Server-Sent Events. Clients connect to mcp.travsr.com via OAuth and receive a persistent SSE stream.

Incremental reindex

The git post-commit hook calls travsr hook-run --from-hook. The delta indexer:

  1. Reads the diff between HEAD and HEAD~1
  2. Computes the SHA-256 of each changed file's contents and compares it with the hash recorded in the files table, so a file the diff lists but whose bytes are unchanged is skipped
  3. Re-parses only the files that actually changed
  4. Writes only changed nodes and edges to the graph
  5. Updates the edge index

Because only the changed files are re-parsed, an incremental reindex touches a small fraction of the graph rather than rebuilding it.