Taguru
reference · code map

Code map (taguru-code)

taguru-code is a standalone binary that turns a git repository into a queryable code map — where is this symbol, what does this file contain — with no server, no LLM, and no configuration (ADR 0010). By default it is fully offline; the one opt-in network path is pointing TAGURU_EMBED_URL at a remote embeddings endpoint (see Environment) — unset, or set to local, nothing ever leaves the host. It answers in milliseconds what an agent would otherwise grep for, and ships its own accuracy gate so "does find still find things" is a CI check, not a hope.

Binary: taguru-code (same crate as the server) Data: $REPO/.taguru — a normal taguru data directory Languages: Rust today (the grammar table is per-extension)

Quick start

# inside any git work tree
taguru-code sync                    # ingest the repo (idempotent; re-run any time)
taguru-code find parse_batch        # kind, qualified name, file:line — best match first
taguru-code tree src/ingest.rs      # what a directory / file / symbol contains
taguru-code status                  # is the map current? pending commits + dirty files
taguru-code watch                   # keep syncing as the tree changes (default every 2s)

Add .taguru/ to your .gitignoresync reminds you until it is. The map never indexes its own storage either way; gitignoring it just keeps git status quiet.

The verbs

sync, watch, find, tree, status, evalset, and eval all accept --context NAME (default code) and --data-dir PATH (default $REPO/.taguru) — several maps can share one directory, or one map can live somewhere else entirely. models takes only --json.

  • sync [PATH] [--dry-run] — ingest the repo at PATH (default .). Incremental: only files changed since the last sync (committed churn plus the dirty set) are re-parsed, and re-ingesting an unchanged file is a no-op by the import contract's retract-then-apply. --dry-run reports what would change and writes nothing. The passage log is compacted automatically once it outgrows the snapshot, so a long-lived map does not grow without bound.
  • watch [PATH] [--interval-ms N]sync once, then keep rescanning (default every 2000 ms) until stopped. An edit racing the initial sync is caught by the next pass — the map converges, it never wedges.
  • find <cue> [--json] [--limit N] — locate a symbol: kind, qualified name, file:line, best match first (default 10 rows). The cue can be a bare tail (parse_batch), a qualified suffix (Type::method, file.rs::name), a path fragment, or a near-miss typo — see lookup for the exact tiers. --json emits one JSON object per row for tooling.
  • tree [PATH] — list what a directory, file, or symbol contains, one level of the same contains edges sync wrote; with no PATH, the roots (entries contained by nothing).
  • status — the map's freshness, honestly: the last synced commit, pending committed changes, and pending dirty files. The map is a snapshot as of the last sync; status is how staleness stays visible instead of silent.
  • evalset --out FILE [--sample N] — sample symbols from the synced map (default 200) and emit labeled eval cases: tail cues, qualified cues, and path cues. Tails shared by too many symbols sample as qualified cues — no tool can answer tests from the bare name, and no agent would ask that way.
  • eval --eval FILE [--thresholds FILE] — replay the cases through the same find core the agent uses and score hit@1 / hit@10 / locator drift. With --thresholds, a regression exits 3 (the ADR 0004 §5 CI convention, offline); unknown threshold keys or mis-typed values hard-error, and the run reports how many thresholds applied — a silently hollow gate is worse than none.
  • models [--json] — list the local embedding models this build carries, best pick first (the same catalog TAGURU_EMBED_MODEL accepts).

The universe: exactly what ripgrep sees

The map covers tracked plus untracked files, minus everything .gitignore excludes, read as the bytes on disk — staged and unstaged edits included.

  • One authority for membership: git ls-files --cached --others --exclude-standard. There is no hand-rolled walker — gitignored files (secrets, build output, local settings) never enter the map, and a directory that is not a git work tree is a refusal, not a fallback.
  • The equivalence is the point: the tool's visibility matches the agent's other eyes, so "taguru-code knows everything rg would search" holds without caveats — including the code the agent wrote this session, before any commit.
  • HEAD is only the incremental anchor, never a content source: a sync touches the committed churn since the last anchor plus both dirty sets (now and as of the last sync), so reverts, deleted untracked files, and dirty-then-committed files all heal to their real content. A gc-orphaned anchor degrades to a full re-sync with a warning — idempotency is the recovery mechanism.
  • .taguru/ is excluded unconditionally — correctness does not depend on the .gitignore advice above.

Where the map lives

$REPO/.taguru is a normal taguru data directory holding a context named code: the same image, WAL, and passage-log formats the server uses, written through the same batch/import contract a client could have sent. Nothing about it is bespoke — point taguru serve at it and the map is queryable over HTTP and MCP like any other context; delete it and the next sync rebuilds it whole.

What a file becomes

One file = one source (the retract-then-apply unit), carrying location and structure — nothing cleverer (ADR 0010 §6).

  • Names: a symbol is <repo-relative path>::<name path> — the file path IS the namespace, so identity is language-agnostic and collision-free without module-resolution cleverness. Files and directories are their bare paths.
  • Edges: defined_in (symbol → file, the "where is X" product) and contains (directory → file → symbol, one uniform chain). No call graph, no module-dependency facts in v1 — coarse facts age slowly; fine-grained ones churn on every edit and duplicate what LSP-shaped tools already do well.
  • Line ranges ride as citation locators ({"kind": "lines", "value": "a-b"}) — the open locator vocabulary of ADR 0007 §7, zero server change.
  • The passage is not the raw file: each symbol contributes its one-line signature as one paragraph, in source order — so paragraph i is symbol i by construction, every symbol keeps its own locator, and BM25 indexes the identifier-dense line that matters.
  • Extraction is AST-mechanical (tree-sitter, one grammar per file extension — Rust today; the extension table is the one place a language plugs in). No LLM anywhere in the pipeline, which is what makes the map deterministic and the accuracy gate cheap.

Lookup without aliases

Short-name aliases were the obvious design and are rejected (ADR 0010 §7): the alias store is append-only, and short names collide constantly in code (new, run, tests) — a one-shot, irreversible claim is the wrong primitive for names that churn. Instead find ranks at read time, over one in-memory scan of the defined_in edges, tier by tier:

  • exact tail segment (parse_batch),
  • qualified suffix (Type::method, or file.rs::name across a path boundary),
  • tail prefix / substring,
  • file/directory basename, for path-shaped cues,
  • tail-segment bigram-Dice as the typo fallback — with a floor. Below the floor the honest answer is "no match, fall back to grep", never plausible-looking junk.

A renamed file's old symbols are ghosts no source attests anymore — read paths filter attribution-less edges, so find never serves them.

The accuracy gate

evalset samples labeled cases from the same parse; eval replays them through the same find core and scores hit@1 / hit@10 / locator drift; --thresholds turns a completed run into a pass/fail gate that exits 3 on regression. Acceptance at merge on this repository's own 200-case set: hit@1 0.915, hit@10 1.00, drift 0.

Environment

  • TAGURU_EMBED_URL / TAGURU_EMBED_MODEL — unset, the semantic lane is off and nothing touches the network. local embeds symbol signatures in-process during sync, still fully offline (default builds carry the models; slim --no-default-features builds warn and keep the lane off). Any other URL is an OpenAI-compatible /embeddings endpoint — with a remote URL, sync sends each symbol's one-line signature (its identifier-dense source line) to that endpoint; do not point it at a third party you would not paste code to. Pick a model from taguru-code models.
  • TAGURU_USAGE_LOG0/false/off disables the per-invocation usage log. On by default: one JSONL record per invocation (verb, arguments, hit count, output bytes) so "is the map actually getting used, and does it answer" has data behind it.
  • TAGURU_USAGE_LOG_DIR — where usage-*.jsonl records go (default $HOME/.taguru/logs).
  • TAGURU_USAGE_LOG_MAX_BYTES — total cap across the usage files; oldest days are deleted first and today's file always survives (default 52428800 = 50 MiB, 0 = uncapped).

Current limits

  • Rust only, today. The grammar table routes by file extension and currently claims .rs alone; other languages are the tracked follow-up (#443), and a new language is one Grammar impl plus one table row.
  • The map is a snapshot as of the last sync. A mid-edit file may parse partially until the next pass — status shows the gap, watch closes it continuously.
  • Location and structure only. No call graph, no type resolution — by design; pair the map with grep and LSP tools rather than expecting it to replace them.