Code as a Graph
Before any clever algorithm, travsr needs one thing: a clean way to say “this exact piece of code” and “this connects to that.” Three ideas do all the work - nodes, edges, and a globally-unique address.
1. A node = one named thing in your code
A function, a method, a class, a file, an import. Each becomes a single
dot (a “node”) in the graph. In our payment example, validateCard() is a node,
PaymentService.charge is a node, and so on.
A node is a house on a street. It has an address, and roads lead in and out of it.
2. The address: a “VName”
Every node gets a five-part address called a VName (a “virtual name”, borrowed from Google’s Kythe project). It’s globally unique, even across different repos and languages, so two tools always agree on what they’re pointing at.
The five parts
| Part | Means | Example |
|---|---|---|
corpus | which project | github.com/acme/shop |
root | build root / branch | "" |
path | file | src/payment.ts |
language | language | typescript |
signature | the symbol | method:PaymentService.charge |
Turned into a short ID
That five-part address is hashed (with BLAKE3) into a single 64-bit number called a NodeId. Same code → same ID, every time. That’s how travsr knows a function is “the same one” after you edit a different part of the file.
VName {
corpus: "github.com/acme/shop",
path: "src/payment.ts",
language: "typescript",
signature: "method:PaymentService.charge",
}
│ BLAKE3 hash (first 8 bytes)
▼
NodeId(0x9f3a…) ← the primary key
crates/travsr-core/src/lib.rs · VName::id()
The VName is the full postal address. The NodeId is the barcode the post office prints from it, short, unique, and machine-fast to sort by.
Because the ID comes from what the code is (not a random counter),
travsr can re-index just the files that changed and the IDs still line up with everything else.
There’s even a version byte baked into the hash (SIGNATURE_FORMAT_VERSION = 2), bump it,
and every old database is intentionally invalidated so nothing stale is ever served.
3. Edges: the roads between nodes (and their importance)
An edge is a directed link: “A calls B”, “A imports B”, “A overrides B”. Crucially, not all roads are equal. A direct function call is a strong, meaningful link; a loose import is weaker. travsr bakes this into a number, the PPR weight - that the algorithms use later to decide what matters.
| Edge kind | Weight | Plain meaning |
|---|---|---|
RefCall | 1.00 | A directly calls B, the strongest signal |
FFICall | 0.85 | A calls B across languages (e.g. TS → Rust) |
DefinesBinding | 0.70 | A contains B (file contains function) |
Exports | 0.60 | A module exposes B |
Depends / ResolvesTo | 0.50 | imports / import-resolves-to-file |
RefImports / IsImplementation | 0.40 | named import ref / implements interface |
Configures | 0.35 | a config file (JSON/YAML/TOML/XML) configures a symbol |
Overrides | 0.30 | method overrides a parent method |
ExternalDependency | 0.30 | config points at an external package, weakest |
data as nodes too travsr doesn't only parse
code. Config and data files, JSON, YAML, TOML, and XML, are parsed into Phase A nodes,
and the two edge kinds above (Configures / ExternalDependency) wire them into
the same graph, so "what reads this config key?" is a real graph query.
Thicker, brighter arrows carry more weight. The number on each arrow is the exact weight from the code. These weights are the “road sizes” the PageRank walk follows in Chapter 3.
Where it all lives
The whole graph is stored in a plain SQLite database file at
.travsr/graph.db right next to your code. Three core tables:
nodes
id, corpus, path, language, signature, kind. One row per code thing.
edges
src, dst, kind. One row per relationship. Indexed both directions for fast “who calls me?”.
files
path, sha256, last_indexed_at. The hash is how travsr skips unchanged files (next chapter).
It uses SQLite in WAL mode (write-ahead logging) so reads and writes don’t block each other, the daemon can update the graph while the AI is querying it.