Point CodeNavigator at any repo and ask "where's the JWT refresh handled?" or
"what calls issue_jwt?" β it answers with citations to real file:line
locations. Everything runs locally: ONNX embeddings on CPU, a transparent vector
store, BM25 keyword search, a cross-encoder re-ranker, and a tree-sitter call
graph across 9 languages. Small enough to read in one sitting, structured so
every piece is swappable as you go deeper.
- π Hybrid retrieval β semantic embeddings + BM25 keyword search, fused with Reciprocal Rank Fusion, then cross-encoder re-ranked
- π³ Structure-aware chunking via tree-sitter (Python, JS, TS/TSX, Rust, Java, C#, C++, Go) with a regex fallback
- π§ Code intelligence β
defs/callers/callees, plus graph-aware answers that pull in the code your matches actually call - β‘ Incremental indexing β hashes files, re-embeds only what changed
- π Eval harness β recall@k / MRR across modes,
--scaffoldfor curated sets,--fail-underCI gate - π₯οΈ CLI + desktop app (Tauri) over one shared engine
cd CodeNavigator
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[treesitter]" # includes parse-tree chunking (recommended)
# or: pip install -e . # regex-heuristic chunking only, zero extra deps
# 1. Build the index for a repo (first run downloads a ~130MB ONNX model, once)
codenav index /path/to/your/repo
# Re-running is incremental: only changed/new/deleted files are touched, so
# it's near-instant. Force a clean rebuild with --full.
codenav index /path/to/your/repo # fast: skips unchanged files
codenav index /path/to/your/repo --full # rebuild everything
# 2a. Retrieval only β no API key needed, great for seeing what RAG returns
codenav search /path/to/your/repo "where is the jwt refresh token handled"
codenav search /path/to/repo "refreshToken" --mode lexical # exact identifier
codenav search /path/to/repo "auth flow" --mode vector # semantic only
# default --mode is hybrid (vector + BM25 keyword, fused)
# 2b. Full answer via Claude (needs an API key)
export ANTHROPIC_API_KEY=sk-ant-...
codenav ask /path/to/your/repo "how does auth refresh work? cite files"
codenav ask /path/to/repo "..." --no-rerank # skip the cross-encoder stageThe index lives in <repo>/.codenavigator/ β add that to your global gitignore.
Prefer a GUI? See desktop/ for a Tauri app over this same engine.
Duplicated build output (bundled JS, dist/ copies) pollutes retrieval β the
index keeps returning duplicate copies instead of the source. Drop a
.codenavigatorignore (gitignore-flavored) at the repo root to keep them out:
client-dist/
public/js/
client/public/js/
*.min.js
**/vendor/**Structural questions, no embeddings or API key needed β built during index:
codenav defs /path/to/repo AuthService.login # where is it defined
codenav callers /path/to/repo issue_jwt # what calls it
codenav callees /path/to/repo AuthService.login # what it callsSame-file calls resolve exactly; cross-file calls resolve by name and report
all candidates when a name is defined in several places. Add --json to any.
Turn "does hybrid/rerank help" into numbers on your own repo:
codenav eval /path/to/repo # auto name->code benchmark
codenav eval /path/to/repo --rerank # also score the hybrid+rerank mode
codenav eval /path/to/repo --curated q.jsonl --kind bothPrints recall@1, recall@k, and MRR per mode (vector, lexical, hybrid,
optionally hybrid+rerank). The auto benchmark needs no labeling β it turns
each function name into a query and uses that function as the gold answer.
For realistic questions, write a curated JSONL (one object per line):
{"query": "where is JWT refresh handled?", "path": "svc/auth.py", "start_line": 40, "end_line": 58}Read the name-based numbers as "can it map a concept phrase to the right function" β the name's words appear in the code, which favors keyword search, so use a curated set for the cleanest semantic comparison. Example run (lexical only, real BM25) across a few repos: recall@10 lands 0.94β1.0, but recall@1 varies a lot by repo β low recall@1 with high recall@10 is the signature of similarly-named symbols, and the repo where semantic retrieval helps most.
Scaffold a curated set so you're not starting from a blank file β it emits template rows for the repo's meatier functions with real paths/lines to attach questions to:
codenav eval /path/to/repo --scaffold --max-items 30 > evals/myrepo.jsonl
# then edit each "query": "TODO: ..." into a real questionA starter curated set for smart-learning-advisor lives in
evals/smart-learning-advisor.jsonl (18 hand-written questions).
Gate CI on retrieval quality β fail the build when a change regresses it:
codenav eval /path/to/repo --check-mode hybrid \
--fail-under "recall@10=0.6,mrr=0.35" # exits 1 if below.github/workflows/ci.yml wires this up: one job runs the unit tests, a second
indexes a corpus and runs the gate (calibrate the thresholds after the first
green run β set them ~10β15% below your observed numbers).
repo ββΊ chunker ββΊ embedder ββΊ store βββ
split on text->vec numpy βββΊ fuse (RRF) ββΊ rerank ββΊ LLM
functions (fastembed) cosine β vector + cross- grounded
lexical ββ keyword encoder answer
(BM25)
chunker.py+treesitter.pyβ structure-aware splitting. With thetreesitterextra installed, files are parsed into real syntax trees: callables are emitted whole (decorators/exportincluded, nested closures kept with their parent), and containers are split into a header chunk plus one chunk per method with a qualified name (SessionManager.invalidate). Without the extra, it falls back to a dependency-free regex heuristic. Both produce identicalChunkobjects. Supported grammars: Python, JS, TS/TSX, Rust, Java, C#, C++, Go. This is where 80% of retrieval quality is won.embedder.pyβ wraps fastembed (ONNX, CPU, no PyTorch). DefaultBAAI/bge-small-en-v1.5, 384-dim. Uses BGE's query/passage prefixes, which measurably improves retrieval.store.pyβ a vector store you can see through: unit vectors in a numpy matrix, metadata in SQLite, "search" is one dot product + argsort. This is what a vector DB is, minus the marketing.lexical.py+ hybrid retrieval β a hand-rolled BM25 keyword index with a code-aware tokenizer (splitsrefreshToken/refresh_tokeninto subwords). By defaultquery.pyruns both vector and BM25 search and fuses them with Reciprocal Rank Fusion, so semantic questions and exact identifier lookups both land. Choose one with--mode vector|lexical|hybrid.rerank.pyβ two-stage retrieval. An optional ONNX cross-encoder re-scores the fused top ~30 candidates by reading query+chunk together, then keeps the best k. On by default; skip with--no-rerank. Falls back to the fused order if the model isn't available.index.pyβ builds the index incrementally. It hashes every source file and compares against the{path: hash}manifest from the last run, so only changed, new, or deleted files are re-chunked and re-embedded. Embedding is the one expensive step; everything else is cheap, so a re-index after a small edit is near-instant. Switching embedding models triggers a full rebuild automatically (mixing vectors from two models would corrupt search).callgraph.pyβ code intelligence. Reuses the tree-sitter parse to extract definitions and call sites, then resolves each call to its definition: exact within a file, name-based across files (reporting all candidates when ambiguous rather than guessing). Powers thedefs,callers, andcalleescommands β and graph-awareask: retrieval finds the code a question is about, then the graph pulls in the code that code calls (marked\u2190 called by X), so the LLM answers from the real implementation, not just lexical look-alikes. On by default forask;--no-expandto disable.llm.pyβ hands retrieved chunks to Claude with a strict "answer only from context, cite locators" prompt.desktop/β a Tauri 2 desktop app over this engine (Rust backend runs the CLI with--json, static web frontend renders it). Seedesktop/README.md.
pip install pytest numpy && python -m pytest -qThe test injects a fake embedder, so the full chunkβembedβstoreβretrieve loop runs with no network and no API key.
Tree-sitter chunkingβ done (treesitter.py). Next grammar-side win: capture interspersed top-level statements between defs, and addtype_alias/recordnode types where you need them.Incremental indexingβ done (index.py). Next: the store still rewritesvectors.npyin full on each save; append-only persistence would make large-repo writes cheaper too.Hybrid retrievalβ done (lexical.py, RRF fusion inquery.py).Cross-encoder re-rankerβ done (rerank.py, two-stage inquery.py).Desktop shellβ done (desktop/, Tauri 2 over the CLI).Call graph / code intelligenceβ done (callgraph.py:defs,callers,callees). Next: import-aware resolution to disambiguate cross-file candidates, and receiver-type tracking for method calls.Graph-awareβ done (ask_expand_with_graphinquery.py): the top hits' callees are pulled into the LLM context. Next: expand callers too for "impact of changing X" questions, and go one hop deeper when budget allows.Evaluation harnessβ done (eval.py:codenav eval, recall@k / MRR across modes;--scaffoldfor curated templates;--fail-underCI gate in.github/workflows/ci.yml). Next: a docstring->code dataset (less name leakage) and per-query drill-down to see what each mode misses.Exclude built/vendored copies from indexingβ done (.codenavigatorignore, gitignore-flavored, honored by indexing, the call graph, and eval). Duplicate copies are a measurable recall drag.- Swap the store β LanceDB/Qdrant behind the
VectorStoreinterface once you outgrow brute-force numpy (~50k+ chunks). - Desktop graph panel + streaming progress β expose the graph verbs in the
Tauri app (the
--jsonoutput is ready) and stream index progress as events.
- Top-level statements between two definitions aren't chunked (only the module preamble before the first definition is). Rare in the supported languages; the fallback windows anything with no definitions at all.
- A grammar not in the
treesitterextra falls back to regex automatically.
| Env var | Default | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
β | required for ask |
CODENAVIGATOR_MODEL |
claude-sonnet-5 |
answer model |
--model flag |
BAAI/bge-small-en-v1.5 |
embedding model |