Enabling terraphim-grep Offline (lessons learned)
Date: 2026-06-29
Scope: Getting terraphim-grep to return real results offline, with a project-specific knowledge graph, no server required.
This is an operational lessons-learned note, not a design doc. It captures the
exact failure mode that makes a freshly-installed terraphim-grep return zero
results for every query, and the minimal setup to get KG-driven search working
against any project.
TL;DR — the one thing that bites everyone
A stock cargo install terraphim_grep binary does not search code.
terraphim_grep's Cargo.toml gates the actual code scanner behind an opt-in
feature:
[features]
code-search = ["dep:fff-search"] # fff-search = the code scanner
default = ["llm"] # <-- code-search is NOT defaultThe default install compiles search_code() down to a stub that returns
Ok(vec![]):
// terraphim_grep/src/hybrid_searcher.rs
async So every query returns chunks_returned: 0 in search_latency_ms: 0, with
no error. Rebuild with the feature enabled:
That is the fix. Everything below is diagnosis and project setup.
Symptom → diagnosis → fix
| Symptom | Meaning | Fix |
|--------|---------|-----|
| chunks_returned: 0, search_latency_ms: 0, no error | fff-search is compiled out (no code-search feature) | rebuild with --features code-search |
| chunks_returned > 0 but kg_hits: 0 | fff-search works; thesaurus not loaded / not matching | pass --thesaurus; check synonym terms |
| Graph has no nodes yet - no documents have been indexed | rolegraph has no document nodes | ignore for grep — the thesaurus-only fallback still boosts; only matters for terraphim-agent |
| No thesaurus specified and could not find default | --thesaurus missing and no auto-discovered file | pass --thesaurus, or place .terraphim/thesaurus-<role>.json |
| Failed to compute source hash for ...kg | broken symlinks in the global KG dir poison the build | find ~/.config/terraphim/kg -xtype l -delete |
The single reliable signal that fff-search never ran is
search_latency_ms: 0 combined with chunks_returned: 0 for a query that
must exist in the codebase. Real scans take single-digit-to-hundreds of ms.
Architecture: why it works offline (and what fooled me)
terraphim-grep is offline-first. The terraphim-agent help calls itself
"server-backed", which mislead me into thinking grep also needed the server. It
does not.
terraphim-grep "query" --paths . --thesaurus t.json
│
HybridSearcher::search() (parallel tokio tasks)
├── search_code() → fff-search over --paths → code_results (THE chunks)
└── search_kg() → rolegraph query
└─ if graph empty → thesaurus-only fallback
│
boost_chunks_with_kg(code_results, kg_concepts) ← KG only RE-RANKS
│
resultsTwo consequences worth internalising:
-
Chunks come from fff-search, not from the KG. The thesaurus/rolegraph only boosts (re-ranks) chunks. So an empty rolegraph is not fatal — there is an explicit thesaurus-only fallback in
search_kgso boosting still fires when no documents are indexed. -
The rolegraph rabbit hole is a trap. I burned real time believing the
Graph has no nodesmessage (and the missingterraphim_server) was the blocker. It was a red herring. The real blocker was the feature flag. When grep returns zero, look at the code-search feature first, the graph last.
Source of truth (in the published crate, ~~/.cargo/registry/src/.../terraphim_grep-<ver>/):
src/hybrid_searcher.rs (search, search_code, search_kg) and Cargo.toml.
Project setup: a self-contained .terraphim/
Drop this into any project root. No global config edits required.
.terraphim/
├── kg/ # KG concepts as markdown
│ ├── provider.md
│ ├── session.md
│ └── ...
├── thesaurus.json # compiled synonym -> {id, nterm} map
└── config.json # (optional) Role for --role-configKG markdown format
One concept per file. The synonyms:: line is what the thesaurus is built from:
Thesaurus format (must match exactly)
Each synonym maps to {id, nterm} where nterm is the normalised concept.
All synonyms of one concept share its id/nterm. Generate it from the
markdown by parsing the # Title and the synonyms:: line — a ~15-line
Python script is enough (lowercase keys, dedupe, sequential ids).
Generate from KG markdown
Run it
# structured:
A healthy result:
KG-boosted chunks score above the fff baseline (1.0); a chunk whose path/content matches a thesaurus concept rises to ~3.0, so your project vocabulary directly shapes ranking.
Gotchas checklist
- [ ] Rebuilt with
--features code-search? (Default install is useless for search.) - [ ]
--thesauruspassed and parsed? (Loaded thesaurus with N entriesin debug log.) - [ ]
search_latency_ms > 0? (Zero = fff-search never ran.) - [ ] Global KG broken symlinks cleaned? (
find ~/.config/terraphim/kg -xtype l -delete.) - [ ] Query terms lowercased in the thesaurus? (Aho-Corasick matching is case-sensitive on the keys.)
Enriching the KG with ast-grep (code anchors)
Prose synonyms only get you so far — a query for ReadTool or AnthropicProvider
won't boost the right chunks unless those exact code identifiers are in the
thesaurus. Use ast-grep to mine real identifiers and add them as anchors.
Why it helps
KG-boosting (boost_chunks_with_kg) raises a chunk's score when its path or
content contains a thesaurus key. Adding real code tokens (ReadTool,
ModelRegistry, AuthStorage, SseParser, AnthropicProvider) means a query
for that token matches the concept and ranks the actual definition file
first. Observed: ReadTool → concept tool, tools.rs boosted to 3.0 vs 1.0
baseline; AnthropicProvider → concept provider, providers/anthropic.rs to
4.0 (three concept hits).
Extract with ast-grep
Captured names live at match["metaVariables"]["single"]["NAME"]["text"] (the
top-level JSON is a list of matches; metaVariables.single, not a bare
NAME).
Bucket by concept — selectively
Map each identifier to a concept with name-based rules. Do not use
file-wide catch-alls like f.startswith("src/providers/") — they flood the
thesaurus with peripheral request/response types and metrics noise.
= Append each bucket to the matching kg/<concept>.md synonyms:: line
(idempotently — only add lowercased terms not already present), then regenerate
thesaurus.json. A working, idempotent version of this whole loop lives at
pi_agent_rust's .terraphim/scripts/refresh-anchors.sh — run it whenever the
code changes to keep anchors aligned.
Verify
# expect: concepts include 'tool', top chunk = tools.rs at score > 1.0Lessons, bluntly
- A feature-gated no-op stub is indistinguishable from "working but empty" unless you read the source.
Ok(vec![])behindcfg(not(feature))returns success with zero items — no error, no warning. Always confirm a search tool actually scans (non-zero latency) before debugging results. - "Server-backed" in a sibling tool's tagline does not mean every CLI in the family needs the server. terraphim-grep is offline-capable; the rolegraph-empty messages are advisory, not blocking.
- Match the diagnostic to the layer. Zero chunks → code scanner. Zero boost → thesaurus. Both are independent; fix them independently.
- Keep the project KG in the repo (
.terraphim/), version-controlled. It is documentation that doubles as a search index. - The best thesaurus terms are the code's own identifiers. Prose synonyms match intent; ast-grep-mined struct/trait/impl names match the actual text in chunks. Enrich the KG structurally and keep it fresh with a repeatable script — a hand-maintained thesaurus rots.
- Selectivity beats coverage. File-wide bucketing rules (
f.startswith("src/providers/")) drown the thesaurus in noise; name-based rules (n.endswith("Provider")) keep it signal-rich.
Companion documents
- Skill:
terraphim-skills/skills/terraphim-grep-offline/SKILL.md-- agent-facing skill covering offline search, thesaurus setup, ast-grep enrichment, and the OpenRouter--answerLLM fallback (model selection, env vars, 1Password key loading). - Usage guide:
terraphim-skills/docs/terraphim-grep-usage.md-- end-user quick reference with the OpenRouter model cost table. - OpenRouter fallback: pass
--answer(optionally--force-rlm) and exportOPENROUTER_API_KEY/OPENROUTER_MODEL; see the skill's "OpenRouter model setup" section.