Dokumentation

So funktioniert greppy

greppy ist ein natives Rust-Binary mit zwei Betriebsarten. Ein Aufruf, der keiner von greppys Befehlen ist, startet das echte System-grep. Alles andere ist ein strukturierter Befehl über einen Index, den greppy für jedes Repository baut und vorhält.

Normales grep

Jeder Aufruf, der kein greppy-Befehl ist, startet das echte grep und gibt Ausgabe und Exit-Code unverändert zurück. Er öffnet keinen Index, lädt kein Modell und fasst keinen greppy-Cache an: In einem leeren HOME legt greppy -rn hello . keine einzige Datei an. ripgrep-artige Aufrufe gehen an ein echtes rg, wenn eines installiert ist. Die Einzelheiten stehen unter grep- und ripgrep-Passthrough.

Ein vorberechneter Code-Graph

Ein indexierter, typisierter Symbolgraph mit den Kanten CALLS, USAGE, TYPE_ASSIGN und IMPORTS beantwortet who-calls, callees, impact und path direkt. Du bekommst aufgelöste Beziehungen mit datei:zeile, keine Textfunde. Dazu kommen Strukturkanten wie DEFINES und CONTAINS_FILE. greppy stats zeigt, was im Graphen steckt.

$ greppy stats
project: serde
files: 339
nodes: 4573
…
  Function 731
…
  Method 950
…
edges: 9427
  BOUND 31
  CALLS 1410
  CONTAINS_FILE 221
  CONTAINS_FOLDER 43
  DEFINES 4814
  IMPLEMENTS 11
  IMPORTS 72
  TYPE_ASSIGN 77
  USAGE 2748

Tree-sitter-Parser für mehr als 60 Sprachen sind ins Binary kompiliert; was jede Stufe extrahiert, steht unter Sprachen. Der Graph liegt in einem lokalen SQLite-Store außerhalb deines Repositorys. Vollständige Quelltexte kopiert greppy nicht hinein: Die exakte Codesuche (search-pattern) liest den aktuellen Worktree über das echte grep.

Semantische Suche und Navigationshinweise

Jedes Binary enthält zwei Modelle:

Modell Aufgabe
Googles EmbeddingGemma (300M) search findet Definitionen über ihre Bedeutung, also Code, den du beschreiben, aber nicht benennen kannst
Qwen3.5-0.8B (Q4_K_M, MTP), bei greppy per Destillation feinjustiert schreibt einen kurzen Zweck-Hinweis unter jede zurückgegebene Funktionssignatur und zu jeder Definition, die brief ausgibt

Die Gewichte liegen auf Hugging Face (EmbeddingGemma, greppy-Qwen3.5), sind per SHA-256 in MODEL_ASSETS.json festgenagelt und werden vor dem Build geladen. Release-Pakete enthalten sie schon. Zur Laufzeit lädt greppy nichts herunter.

Die Inferenz ist lokales Rust mit mitgelieferten Metal- und CUDA-Kernels: keine llama.cpp-Runtime, kein Python, kein HTTP, kein Modellserver. Produktiv läuft sie auf Metal unter Apple Silicon und auf CUDA unter Linux mit NVIDIA-GPU.

Belege und Hinweise

Quelltext-Bytes und Graph-Beziehungen sind die Belege. Die Hinweise sind es nicht: In der Ausgabe ist ein Satz nach einem Gedankenstrich ein generierter Hinweis, kein Quelltext. greppy behandelt Hinweise als nicht vertrauenswürdig und lässt sie weg, wenn Inferenz oder Validierung scheitern. Eine gescheiterte Inferenz entfernt nie deterministischen Quelltext oder Graph-Ausgaben.

brief zeigt beides nebeneinander. visit_content_map hat keinen Doc-Kommentar, also ist der erste Satz ein generierter Hinweis. Die Signatur ist wörtlicher Quelltext, die Zeile 1111 nennt die Aufrufstelle mit dem aufgelösten Symbol, dahinter steht wieder ein Hinweis.

$ greppy brief visit_content_map
Visits a map of content items and returns the resulting value.

serde/src/private/de.rs:1103-1110
    fn visit_content_map<'de, V, E>(
        content: Vec<(Content<'de>, Content<'de>)>,
        visitor: V,
    ) -> Result<V::Value, E>
    where
        V: Visitor<'de>,
        E: de::Error,
    {
  1111  MapDeserializer::new — constructs a MapDeserializer from a vector of Content values
    }

called by ContentDeserializer::deserialize_any, ContentDeserializer::deserialize_map and ContentDeserializer::deserialize_struct

Geteilte Inferenz-Daemons

Indexierung, Suche, Hinweise und bash-smart schicken Modellarbeit an nutzerweite lokale Daemons über einen Unix-Domain-Socket. Client-Prozesse laden nie selbst ein schweres Modell als Rückfallebene. Anfragen werden in Micro-Batches gebündelt und reihum pro Client abgearbeitet. Die Warteschlange hat keine Kapazitätsgrenze, an der Anfragen abgelehnt werden.

Ein Modell bleibt 300 Sekunden ohne Anfrage geladen; ein Daemon-Prozess beendet sich nach 1800 Sekunden Leerlauf. greppy doctor --json zeigt für jeden Daemon Backend, Zustand, Socket und Warteschlangen-Regel.

$ greppy doctor --json | jq '.inference.daemons | map_values({backend, state, queue_policy})'
{
  "embedding": {
    "backend": "metal",
    "state": "ready",
    "queue_policy": "fair-round-robin-unbounded"
  },
  "summary": {
    "backend": "metal-q4k-forward",
    "state": "ready",
    "queue_policy": "fair-round-robin-unbounded"
  }
}

Seit 0.4.1 laufen Embeddings und Hinweise zusätzlich über einen nutzerweiten, inhaltsadressierten Cache, gebunden an exaktes Modell, Prompt, Aufgabenprofil und Eingabe. Eine unveränderte Definition wird über Repositorys und Worktrees hinweg einmal berechnet; die Daemons sehen ausschließlich echte Cache-Fehlschläge. greppy index weist die Aufteilung aus. Hier indexiert greppy eine frische Kopie von serde, das auf diesem Rechner schon eingebettet war.

$ greppy index .
…
embedded 2443 code spans (0 local-store reused, 2443 global-cache hits, 0 inference misses, 4573 considered, 2150 non-definition skipped, 0 missing-file, 0 invalid-span, 0 oversize, 0 failed, 0 stale pruned)

Der Lebenszyklus des Index

Der Graph wird einmal pro Repository gebaut und über Sitzungen hinweg wiederverwendet. Eine erste Graph-Abfrage baut ihn bei Bedarf, Graph- und Symbolbefehle antworten, sobald er veröffentlicht ist. Die Embeddings für die semantische Suche baut ein ausdrückliches greppy index oder die erste search, die sie vermisst.

In 0.4.1 ist der Lebenszyklus beobachtbar:

  • Eine erste Graph-Abfrage, die auf den Build warten muss, gibt ihre Phasen auf stderr weiter, zum Beispiel writing_graph: 154/219 files; phase ETA about 40s. Kann die Indexierung nicht starten, endet die Abfrage mit dem temporären Code 75 und nennt die Bedingung für den nächsten Versuch.
  • Ein ausdrückliches greppy index meldet sofort seine erste Phase, seine PID und den Status-Befehl.
  • greppy index status --json blockiert während eines Builds nie. Es endet mit 75, meldet "status": "indexing" und einen background_job-Eintrag mit der aktuellen Phase (state), ihrer Einheit (progress_unit), erledigter und gesamter Menge und, sobald bekannt, einer Phasen-ETA.
  • Ein Job, dessen Fortschrittseintrag sich zwei Minuten lang nicht ändert, wird als möglicherweise hängend markiert.

So sieht das aus, wenn ein Skript den Status während eines greppy index . alle 0,25 s abfragt:

greppy index . > /dev/null 2>&1 &
while sleep 0.25; do
  s=$(greppy index status --json); rc=$?
  echo "$rc $(echo "$s" | jq -c '[.status, .background_job.state, .background_job.completed_spans, .background_job.total_spans]')"
  [ $rc -eq 75 ] || break
done
75 ["indexing","extracting_files",0,219]
75 ["indexing","extracting_files",0,219]
75 ["indexing","writing_graph",0,219]
75 ["indexing","writing_graph",0,219]
75 ["indexing","writing_graph",219,219]
75 ["indexing","building_files",0,339]
75 ["indexing","resolving_file_imports",1,1]
75 ["indexing","counting_embeddings",0,0]
…
75 ["indexing","embedding",0,2443]
75 ["indexing","checking_integrity",0,0]
0 ["ok",null,null,null]

Semantische Suche liefert nie Teiltreffer. Automatisierung wartet, bis greppy index status --json mit 0 endet und "healthy": true sowie "embedding_complete": true meldet.

Frische ohne File-Watcher

Jede Abfrage prüft den Index gegen den Working Tree. Passen indexierte Spans nicht mehr zu den Dateien, lehnt greppy ab, statt aus veraltetem Quelltext zu antworten: Eine Abfrage, die den Index nicht auffrischen kann, endet mit dem temporären Code 75 und liefert keine Ergebnisse.

Seit 0.4.1 heilt greppy kleine Änderungen direkt in der Abfrage. Im folgenden Beispiel mit serde fügt ein normaler Editor (kein greppy-Befehl) in serde/src/private/de.rs hinter visit_content_map eine neue Funktion visit_content_map_logged ein, die visit_content_map aufruft. Ohne greppy index kennt die nächste Abfrage den neuen Aufrufer, und die drei bekannten Aufrufstellen stehen auf ihren neuen Zeilen.

$ greppy who-calls visit_content_map
serde/src/private/de.rs:1152  ContentDeserializer::deserialize_any
serde/src/private/de.rs:1399  ContentDeserializer::deserialize_map
serde/src/private/de.rs:1415  ContentDeserializer::deserialize_struct
$ greppy who-calls visit_content_map
serde/src/private/de.rs:1125  visit_content_map_logged
serde/src/private/de.rs:1163  ContentDeserializer::deserialize_any
serde/src/private/de.rs:1410  ContentDeserializer::deserialize_map
serde/src/private/de.rs:1426  ContentDeserializer::deserialize_struct

Eine große Auffrischung startet einen einzigen beobachtbaren Hintergrund-Job, und die Abfrage gibt eine begrenzte Anweisung für den nächsten Versuch zurück, statt einen langen Rebuild zu verstecken. Die Embeddings geänderter Definitionen baut greppy index oder eine search, die sie vermisst. Bis dahin meldet greppy index status --json Exit 73 mit "embedding_complete": false. Im Beispiel oben hat die nächste search die neue Funktion eingebettet und als ersten Treffer geliefert, danach meldete der Status wieder 0. read lehnt einen Graphen, der sich gerade auffrischt, mit einem temporären Fehler ab, statt aktuelle Bytes mit einem veralteten Span zu kombinieren.

Was auf deinem Rechner bleibt

Index, Symbolgraph, Embeddings und Hinweise berechnen die eingebetteten Modelle lokal. Die Code-Navigation macht keine Netzwerkaufrufe, sendet keine Telemetrie und braucht kein Konto. Was gespeichert wird und wie du es entfernst, steht unter Lokale Daten, Datenschutz und Deinstallation.