Skip to content
RAGRAF

Source-available · BUSL-1.1 · v0.3.0

Facts about code instead of blind grep

RAGRAF indexes the repository and answers the LLM agent through fixed operations: where it is defined, who calls it, what breaks on a change. Every fact — with a verifiable file:line reference.

Python 3.13+SQLiteMCP · 59 toolstree-sitter · 29 languagesGPU · torch.sparseBUSL-1.1

92%

target-file hit on the first query

12 of 13, measured

100%

facts with a file:line reference

14 navigation tasks, metric M6

1,0

tool calls per task

was 2.0–3.1

0

files read in full

was 14

Measured on a real PHP portal (14 navigation tasks): bench/results/ and the project README. Call and hit numbers are measurements; "realistically 43" is a modeled correction recorded in the artifact itself.

Problem

The agent works from memory of code it has never seen

Three symptoms known to everyone who has handed an LLM an edit in an existing project. All three are measured, not invented.

It cannot find the place

A name grep yields dozens of candidates, and the right one is not first. VulnGym: in chains longer than 9 steps, in 56.5% of failures the agent never found the file (26.8%) or found the file but not the line (29.7%). More than half of the failures are about navigation, not analysis.

source: measurement

It does not know the DB schema

The column was named "like the variable in the neighboring module" — and that surfaces only at runtime: SQLSTATE[42S22] Unknown column on the very first save. The schema from migrations must be checked before the code runs.

source: real defect

It does not know it is stale

The index was built, then the code was edited — the agent receives facts about a state that no longer exists. "No such function" and "the function appeared after indexing" are indistinguishable in the answer. The decision rests on a false fact.

source: core risk

Query languages do not help

Give the agent Cypher/CPGQL? LLMxCPG (USENIX Security '25): base models generate a valid code-graph query in 19–132 cases out of 1278. A free-form query language turns into an error generator.

source: literature

The conclusion RAGRAF grew from: more than half of agent failures are not about analysis but about navigation. That part must move out of the model — into an external index that answers with facts and knows the boundaries of its knowledge.

Features

What RAGRAF does

A fixed contract instead of free-form retrieval: 52 facade operations, 59 MCP tools, three SQLite stores. Nothing superfluous — and nothing silently broken.

52 fixed operations

symbol, callers, impact, slice, schema, write_check, policy_trace… The agent chooses "what to ask", not "how to ask": there is no graph query language — by measurement, not by taste.

facade: 52 op_* · MCP: 59 tools

Every fact carries proof

path:line, provenance (stated — proven by an analyzer, derived — heuristic), confidence, index_rev. An edge without evidence does not live in the graph as a fact.

link verifiability: 100% (metric M6)

The index knows it is stale

The revision is immutable; edits accumulate in an append-only drift ledger; affected facts are marked stale in every answer; the checksum guard reindexes automatically.

drift ledger · guard · auto-reindex

Hybrid search, fully local

BM25 (FTS5) + 384-dimension vectors (paraphrase-multilingual-MiniLM-L12-v2 via fastembed/ONNX). No torch, no cloud, no subscriptions. The model downloads once (~240 MB).

SQLite: graph + FTS5 + vec0 in three files

DB schema from migrations + write_check

Tables and columns are parsed from migrations/*.sql without DBMS access. write_check verifies the write plan against the schema before running and suggests the nearest existing column names.

44 tables / 453 columns on the pilot

MCP server and honest degradation

59 ragraf_* tools over stdio (JSON-RPC). Without the Joern layer: capability=degraded, slice refuses with a reason; all other operations work. Silent degradation never happens.

optional CPG layer: Joern + JDK 21

GPU-accelerated compute

Two optional GPU paths: embeddings (sentence-transformers+torch, break-even from ~1,800 texts) and tensor graph traversal (torch.sparse CSR, from ~5,000 vertices). Everything runs on CPU without a GPU — the selected backend and its reason are recorded in metadata, never implied.

thresholds: 1,800 texts / 5,000 vertices · backend torch-gpu|torch-cpu|none

29 languages are parsed as trees (PHP, Python, JS, TS, Java, C#, C, C++, Ruby, Lua, Go, Perl, bash, PowerShell, CSS, HTML) and 11 more are attached as text (Markdown, SQL, YAML, JSON, TOML, INI, XML, RST, Rust, batch, plain text).

How it works

Two loops: indexing and answering

Indexing builds a snapshot revision. Answers read the snapshot and verify its freshness. Code edits during work go to the ledger, not to the index.

Indexing (one command)

  1. step 1scancorpus walk, sha256 of every file, the input checksum
  2. step 2asttree-sitter: symbols, calls, SQL literals, line boundaries
  3. step 3resolvename resolution: exact match → name+file → honest refusal
  4. step 4schematables and columns from migrations — no database connection
  5. step 5cpgJoern (optional): interprocedural calls; absent — degraded
  6. step 6lexvecFTS5 tokens and vectors; unchanged chunks reused by hash

The answer to the agent (every request)

  1. step 1operationthe agent calls a fixed operation via MCP or CLI
  2. step 2freshnessthe guard compares corpus_hash; on divergence — stale marks or auto-reindex
  3. step 3factsthe result with path:line references, provenance and confidence
  4. step 4envelopeindex_rev, capability, drift, limits, stale_warnings — in every answer

Three principles derived from measured defects

  • The index is immutable within a revision — facts from different moments in time never mix.
  • Staleness is shown, never hidden: a stale fact cannot be mistaken for a current one.
  • Degradation is never silent: capability and reason ride in every answer.

Architecture

Three SQLite files and zero infrastructure

The entire index state lives in the .ragraf/ directory inside the project. No services, no daemons, no cloud. Delete the directory — delete the index.

graph.sqlite

Nodes (class, function, method), edges (calls, inheritance, imports) with evidence, DB schema, routes. Everything marked rev_id.

lexvec.sqlite

FTS5 lexicon (BM25) and vec0 vectors (384) in one file; raw chunk text — fragments served without touching disk.

control.sqlite

Revisions, the append-only drift journal, reindex archives, call telemetry. Never deleted.

Corpus configuration
# ragraf init <project directory> — creates the config for you.
# The index lands in .ragraf/ inside the project (add it to .gitignore).
project: myproject
root: C:/path/to/your/project
languages: [python, javascript, markdown]
include: ["**/*.py", "**/*.js", "**/*.md"]
exclude: ["**/node_modules/**", "**/build/**"]
secrets_exclude:            # not indexed and not read
  - "**/.env"
  - "**/*.pem"
embedding:
  model: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
  dim: 384
index:
  retain_revs: 3
  joern_enabled: true       # no Joern — capability=degraded

Demo

What it looks like in practice

The scenarios are built on real product outputs (measurements and the project article). These are interface mockups, not screenshots: the CLI and MCP deliberately have no graphics — that is part of the philosophy.

agent request

ragraf -p config/portal.yaml query symbol --name acl_can_act

function · config/acl.php:112–148 · signature: function acl_can_act(array $data): voidconfig/acl.php:112
Parameter contract (from the docblock): array{action:string, user_id?:int, field?:string, to:?string, actor_id?:int} — only action is requiredconfig/acl.php:104

Before this, agents built the call from the signature "array $data" and got a TypeError: the array type says nothing about the key composition.

Mockup. The data is real product output from the project article and the call journal.

Evidence

Measurements, not promises

14 navigation tasks on a real project in two modes: without the index and with it. Below is what changed and how to check it.

28 → 14

tool calls per task

was 28, realistically 43

34 → 1–5

candidates to review

instead of 34 files

14 → 0

files read in full

zero full reads

Real command output: index revision, provenance, path and lines. Nothing rewritten for the showcase.

Panel: 28 corpora, 25 indexed

Revisions, sizes of the three stores, index quality and the reindex queue on one screen. The demo is interactive.

  • path:line — a reference you can open and check
  • provenance — where the fact came from: index, graph or database
  • index_rev — which revision answers the question
  • confidence — how well the fact is confirmed
Open the panel
RAGRAF first screen with the key numbers
First screen
Demo panel with corpus revisions
Demo panel
Facade operations reference page
Operations reference

Source: README, Polygon section; raw artefacts in bench/results. The terminal shows a real command run on this site corpus; the revision is printed in it.

Quick start

Three steps to the first fact

No administrator rights needed: everything installs into the user profile. Verified platform — Windows + PowerShell 7.6; the pip path on Linux and macOS has not been tested.

  1. Installation

    The script creates a venv in ~/.ragraf/venv, installs dependencies and verifies every step with a summary table. Log: ~/.ragraf/logs/.

    PowerShell
    git clone https://github.com/paulmann/RAGRAF.git
    cd RAGRAF
    pwsh -NoLogo -NoProfile -File scripts/install.ps1
  2. Corpus

    ensure finds the corpus, creates one for an unknown directory and rebuilds a stale index — in a single command.

    PowerShell
    ragraf ensure C:\work\myproject
  3. First query

    Global flags -p and --json go BEFORE the subcommand. selftest — a report on 11 components.

    PowerShell
    ragraf -p <config> query search "reindex threshold"
    ragraf -p <config> query symbol --name corpus_hash
    ragraf -p <config> selftest

Flag order

Global flags --project/-p and --json go BEFORE the subcommand (ragraf -p cfg query search …). Exception: ensure has its own --json, placed after it (ragraf ensure <dir> --json).

First indexing with vectors

Downloads the embedding model (~240 MB) once into ~/.ragraf/models. Without internet or with --no-vectors, lexical BM25 search keeps working.

selftest is not a gate

The command always returns exit code 0: it is a report, not a gate. Read the report itself: 9 required components + joern/java (optional).

Corporate TLS proxy

If the model download fails with certificate verify failed — install truststore: install.ps1 -WithTruststore or pip install truststore.

Full command list — ragraf --help. Documentation and measurement protocols — in the repository.

FAQ

Frequently asked questions

Short answers to the questions asked most often. Details — in the repository documentation.

Business Source License 1.1 (BUSL-1.1) — source-available; any use is allowed, including commercial use inside your organisation; offering the product or a derivative to third parties as a hosted or managed service is not allowed. Four years after each release the version moves to Apache-2.0. External dependencies keep their own licenses (tree-sitter — MIT, sqlite-vec — MIT/Apache-2.0, fastembed — Apache-2.0, PyYAML and tiktoken — MIT, ijson — BSD). Java and Joern are installed separately under Apache-2.0 and are not part of the distribution.

Python 3.13 or newer (verified on 3.14); no administrator rights needed — everything installs into the user profile. Disk: ~2.5 GB for the environment (ONNX Runtime, tree-sitter), the index ~1 MB per 1–2 source files. Verified platform — Windows + PowerShell 7.6; the common pip path on Linux and macOS is not verified. The optional Joern layer (interprocedural graph) adds JDK 21 and ~5–6 GB.

The local model sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (384 dimensions) via fastembed and ONNX Runtime — no torch, no cloud. The model downloads once (~240 MB) at the first indexing with vectors into ~/.ragraf/models; the cache directory is overridden by RAGRAF_MODEL_CACHE. Read operations never download the model. Indexing without vectors (--no-vectors) works on lexical BM25 search.

Everything stays on your machine: three SQLite files in the project's .ragraf/ directory, the call journal lives there too. Files in the secrets_exclude list are never opened at all — not indexed, not read, not logged. Telemetry stores a hash of the argument and a short digest instead of full query texts. The only network traffic: PyPI at install time and the one-time model download; there are no API keys and no outbound data flows in the product.

Sixteen languages are parsed as trees (graph nodes and edges): PHP, Python, JavaScript, TypeScript, Java, C#, C, C++, Ruby, Lua, Go, Perl, bash, PowerShell, CSS, HTML. Eleven more are attached as text (findable by search, but they yield no nodes): Markdown, SQL, YAML, JSON, TOML, INI, XML, RST, Rust, batch, plain text. PHP is the most deeply verified — the pilot and the CPG layer (Joern php2cpg) were built on it.

In three commitments. First: the index is immutable within a revision; divergences from the code accumulate in an append-only journal — a stale fact is marked stale in every answer instead of being silently served as current. Second: every fact carries evidence — path:line, provenance (stated/derived) and confidence; heuristics are never mixed with proven links. Third: instead of free-form retrieval answers — fixed operations with explicit limits and a capability state.

There is no graph query language and there will be none — that is architecture decision P4, backed by measurement (LLMxCPG: base models produce 19–132 valid CPGQL queries out of 1278). The 52 operations are a fixed set; extensions are discussed in the repository Issues. The project is developed for concrete tasks, so architecture proposals are welcomed more readily than large patches without a prior conversation.

RAGRAF is itself a local tool: no server is needed for the index or the answers. The MCP server is a stdio process spawned by your agent. The admin panel (optional) listens on 127.0.0.1:8777 only and is unreachable from outside. The only external component is Joern — a persistent server on localhost:9500 during CPG indexing; it stops automatically.

Give your agent verifiable facts

A code index is not "faster search". It moves part of the work out of the model into an external structure that knows what it does not know. RAGRAF's most valuable property: it refuses to answer confidently when it has no right to.

The signature array $data is honest and useless. A docblock with the key composition is useful. A stale: true mark next to a fact is mandatory.