Reproducing the Benchmarks

June 10, 2026 · View on GitHub

This document gives the exact commands to reproduce every benchmark number shown in the README and the diagrams/. Two people running the recipe below on different machines on different days should produce identical numbers, within float rounding.

If you get different numbers, that's a bug — please file an issue.

Verifying the "saved tokens" number

The CLI's Token Savings panel uses a chars / 4 approximation labelled estimated: true, not a model-specific tokenizer. The approximation is designed to be both fast (no model load, no inference) and conservative.

How to verify against a real tokenizer

pip install tiktoken
code-review-graph detect-changes --brief --verify

The panel grows a Verified (tiktoken) row showing the same calculation done with OpenAI's cl100k_base tokenizer (the GPT-4 family). If the estimate is significantly off, you'll see it immediately:

┌───────────────────────── Token Savings ─────────────────────────┐
│ Full context would be:     12,921 tokens                        │
│ Graph context used:           762 tokens                        │
│ Saved:                     12,159 tokens (~94%)                 │
│ Verified (tiktoken):       10,835 tokens (~93%)  [11,611 → 776] │
│ Breakdown: Functions 244 · Tests 191 · Risk 244 · Other 83      │
└─────────────────────────────────────────────────────────────────┘

Calibration result (committed)

A one-time calibration across 222 files / 2.2 MB of mixed source (Python, JS, TS, Go, Rust, RST, MD) pulled from the 6 test repos:

Reposample filesbyteschars/4 estimatetiktoken realratio est/real
flask46470,179117,559109,9691.069
fastapi38156,22439,07234,8971.120
gin30471,793117,962132,2960.892
express23296,80574,20783,5750.888
httpx38254,18463,55662,9091.010
code-review-graph47539,206134,820120,7601.116
OVERALL2222,188,391547,176544,4061.005

chars / 4 is within +0.5% of real GPT-4 tokens in aggregate. Per-repo it swings between -11% (gin: lots of short Go identifiers) and +12% (fastapi: heavy docstrings and type hints), but the ratio stabilizes because both sides of the divide are equally biased.

Reproduce the calibration with the snippet in this commit's code_review_graph/context_savings.py:verify_with_tiktoken, or inline-run the --verify flag on any commit.

What is and isn't deterministic

ReproducibleReason
Tree-sitter parsingPure function of input bytes
Node / edge countsDeterministic upserts keyed by qualified_name
FTS5 BM25 scoresDeterministic
Embeddings via all-MiniLM-L6-v2 on CPUModel weights cache-pinned by SHA in HuggingFace cache
Leiden community IDsSeeded — _LEIDEN_SEED=42 in communities.py, override with CRG_LEIDEN_SEED env var
naive_corpus_tokensDeterministic for a fixed git checkout
git clone at a pinned SHADetermines the source-of-truth byte stream

What used to make it non-reproducible (now fixed):

  • commit: HEAD in every code_review_graph/eval/configs/*.yaml — replaced with the pinned latest test-commit SHA per repo
  • git clone --depth 50 silently fell back to wrong commits when the pinned SHAs were beyond the shallow window — now uses full clones with explicit returncode checks
  • Leiden ran with an unseeded RNG — now seeded
  • nextjs.yaml was a misnamed config evaluating this repo — renamed to code-review-graph.yaml
  • FTS5 was created but never populated by the eval framework's full_build call — code_review_graph/eval/runner.py now calls postprocessing.run_post_processing directly

Prerequisites

  • Python 3.10 or newer
  • git on PATH
  • Network access (~600 MB to clone the 6 upstream repos)
  • ~3 GB free disk
  • For the embedding step: roughly 700 MB extra for torch + sentence-transformers

Step 1 — Install with the right extras

git clone https://github.com/tirth8205/code-review-graph
cd code-review-graph

# eval extras: pyyaml + matplotlib (matplotlib only needed for `--report`)
# embeddings extras: sentence-transformers + numpy
uv sync --extra eval --extra embeddings     # or: pip install -e ".[eval,embeddings]"

Step 2 — Run the formal eval

This step clones 6 upstream repositories at pinned SHAs, builds a full graph for each (parser + cross-file resolvers + signatures + FTS5 + flows + Leiden communities), then runs the token_efficiency, impact_accuracy, agent_baseline, and multi_hop_retrieval benchmarks.

uv run code-review-graph eval \
  --benchmark token_efficiency,impact_accuracy,agent_baseline,multi_hop_retrieval

Failure semantics (applies to every benchmark): a thrown tool call is not a measurement. The row is kept in the CSV with status=error for forensics, but excluded from every aggregate. (Two historical bugs made failures look like wins: a thrown get_review_context produced graph_tokens=0 and a ratio of naive/1, and a thrown analyze_changes silently set predicted = changed, guaranteeing recall 1.0. Both are fixed; regression tests live in tests/test_eval.py.)

Expected runtime on an M1/M2 Mac: roughly 8–15 minutes for the build phase, plus seconds per benchmark.

Outputs:

  • evaluate/test_repos/{express,fastapi,flask,gin,httpx,code-review-graph}/
  • evaluate/test_repos/<name>/.code-review-graph/graph.db
  • evaluate/results/<name>_<benchmark>_<date>.csv

Step 3 — Generate embeddings (required for the standalone benchmark)

The standalone token benchmark ships with 5 hardcoded natural-language questions. Without embeddings, hybrid search can't match them and the benchmark silently returns 0× reduction ratios (a loud warning will print).

for repo in express fastapi flask gin httpx code-review-graph; do
  uv run code-review-graph embed --repo "evaluate/test_repos/$repo"
done

Expected runtime: 2–5 minutes total. Vectors live inside the same graph.db.

Step 4 — Run the standalone token benchmark

This benchmark compares all source-file tokens in the repo against 5 search hits + a few neighbor edges for each of 5 sample questions. The ratio answers: how many tokens does the graph let me skip on a typical question?

uv run python <<'PY'
import json
from pathlib import Path
from code_review_graph.graph import GraphStore
from code_review_graph.token_benchmark import run_token_benchmark

results = {}
for repo in sorted(Path("evaluate/test_repos").iterdir()):
    db = repo / ".code-review-graph" / "graph.db"
    if not db.exists():
        continue
    store = GraphStore(str(db))
    try:
        results[repo.name] = run_token_benchmark(store, repo)
    finally:
        store.close()

print(f"{'Repo':<22}{'naive_tokens':>16}{'avg_graph_tokens':>20}{'avg_ratio':>14}")
print("-" * 72)
for name, out in sorted(results.items(), key=lambda x: -x[1]["average_reduction_ratio"]):
    pq = out["per_question"]
    avg_graph = int(sum(r["graph_tokens"] for r in pq) / max(len(pq), 1))
    print(f"{name:<22}{out['naive_corpus_tokens']:>16,}"
          f"{avg_graph:>20,}{out['average_reduction_ratio']:>13.1f}×")

Path("evaluate/standalone_token_benchmark.json").write_text(json.dumps(results, indent=2))
PY

Canonical numbers

Captured 2026-05-25 on macOS arm64, Python 3.11, sentence-transformers 5.5.1, all-MiniLM-L6-v2, CRG_LEIDEN_SEED=42. If your numbers differ by more than rounding, something in the chain has drifted — file an issue.

Standalone token benchmark (code_review_graph/token_benchmark.py)

Each row is the average of 5 sample questions (how does authentication work, what is the main entry point, how are database connections managed, what error handling patterns are used, how do tests verify core functionality).

Reposnapshot SHAnaive_corpus_tokensavg graph_tokensavg ratio
fastapi0227991a951,0712,169528.4×
code-review-graph84bde354208,8212,49593.0×
gin5c00df8a166,8681,99091.8×
flaska29f88ce125,0221,98671.4×
expressb4ab7d65135,9553,46540.6×
httpxb55d463589,4922,43838.0×

Range across 6 repos: 38× – 528×. The numbers shifted down from a previous capture because (a) the test repos are now wiped/re-cloned from scratch — no leftover build artifacts or local caches inflate the naive baseline; and (b) the embedding text per node became richer in this same release (see embeddings._node_to_text), so the graph response itself is slightly bigger. Both are correctness improvements over the prior numbers.

Formal token_efficiency benchmark (code_review_graph/eval/benchmarks/token_efficiency.py)

A different denominator: just the changed-file content for each commit, vs the full get_review_context() JSON. For small commits the response is larger than the input (it carries impact-radius edges + source snippets), so ratios here are intentionally < 1.0 — that is not a bug, it measures a different thing than the standalone benchmark.

Raw per-commit CSVs in evaluate/results/<repo>_token_efficiency_*.csv.

Impact accuracy (code_review_graph/eval/benchmarks/impact_accuracy.py)

13 commits across 6 repos. The benchmark emits two ground-truth modes side by side, distinguished by the ground_truth_mode CSV column:

ModeGround truthWhat it tells you
graph-derived (circular — upper bound)changed files + files with CALLS/IMPORTS_FROM edges into them — derived from the same graph the predictor traversesAn upper bound. Recall 1.0 here is partly true by construction, not independent evidence.
co-change (same commit, seed excluded)the other files the author actually touched in the same commit, given a single seed fileIndependent-ish evidence from git history. Expect substantially lower recall.

The canonical numbers below were captured in graph-derived mode only (the co-change mode did not exist at capture time). Treat the recall row as a circular upper bound, not as "100% recall":

Metric (graph-derived mode — circular upper bound)Value
Recall (mean across 13 commits)1.000 (upper bound on every commit)
F1 (mean)0.714
F1 (median)0.667
F1 (min / max)0.455 / 1.000

Canonical co-change numbers will be added after the next full capture — we do not quote them before measuring. Single-file commits are recorded with status=skipped in co-change mode (there is nothing independent to grade against).

The blast-radius analysis over-predicts in some commits (precision ≈ 0.30 in the worst case, where 34 files are flagged for a 10-file change). That is intentional: a missed dependency is worse than an extra reviewed file.

Multi-hop retrieval (code_review_graph/eval/benchmarks/multi_hop_retrieval.py)

11 hand-curated tasks across the 6 repos. Each task is a 2-step tool chain:

  1. hybrid_search(nl_query, limit=10) looks for a starting anchor node.
  2. query_graph(<traversal_pattern>, target=<anchor>) walks one hop along callers_of / callees_of / tests_for / imports_of / etc.

The task scores 1.0 only if both the anchor is found in the top-K and the expected neighbor names are returned by the traversal. Scores 0.0 otherwise (which collapses both "search missed the anchor" and "traversal returned the wrong set" — split those by inspecting anchor_found and neighbor_recall in the per-task CSV row).

RepoTaskAnchor foundRankNeighbor recallScore
code-review-graphcrg-parse-file-callersyes01.001.00
code-review-graphcrg-upsert-node-callersyes41.001.00
expressexpress-create-application-calleesyes11.001.00
fastapifastapi-route-handler-callersyes61.001.00
fastapifastapi-get-dependant-callersno0.000.00
flaskflask-dispatch-callersyes31.001.00
flaskflask-exception-callersyes51.001.00
gingin-serve-http-calleesyes51.001.00
gingin-context-next-callersyes01.001.00
httpxhttpx-client-request-callersyes01.001.00
httpxhttpx-async-request-testsyes71.001.00

Average score across 11 tasks: 0.909. 10/11 tasks pass; the one remaining miss (fastapi-get-dependant-callers) targets a function spelled get_dependant ("dependant" with an a) from a query phrased as "dependency declarations into a tree" — there is no lexical overlap and no extractable identifier in the query for the boosting heuristic to lock onto. Left as an honest miss; the fix would be either query rewriting or a richer embedding model.

How the score went from 0.545 to 0.909 (the same-day fix)

The v1 scaffold first scored 0.545 (6/11). Two changes brought it to 0.909 (10/11), both deterministic, both small, both committed in this same session:

  1. embeddings.py:_node_to_text — the embedded text per node used to be just "{name} {kind} in {parent}". It now also includes the dotted form (APIRoute.get_route_handler), the identifier split into words (get route handler), and the enclosing module directory (routing, fastapi, dependencies). All re-embeddings are automatic — the text hash changes, EmbeddingStore.embed_nodes re-embeds. See _split_identifier for the casing/separator rules.

  2. search.py:extract_query_identifiers — natural-language queries like "Who advances the gin middleware chain via Context.Next" now have their dotted / snake_case / CamelCase identifier tokens extracted. Search results whose qualified_name contains any extracted identifier get a 2.0× boost. This pushed Context.Next from rank 11 to rank 0.

The remaining fastapi-get-dependant-callers failure cannot be fixed by either change because the query doesn't share any identifier or substring with the target — that's the boundary of the heuristic.

This benchmark is a v1 scaffold (11 tasks). The intent is to track the multi-hop tool chain as the agent's actual usage pattern rather than just single-shot retrieval. Adding more tasks: append multi_hop_tasks: entries to any config under code_review_graph/eval/configs/*.yaml with the schema:

multi_hop_tasks:
  - id: my-task-id                # required, unique
    nl_query: "natural language" # required, what an agent would ask
    anchor_qualified_suffix:     # required, lowercased suffix of expected
      "rel/path.py::owner.symbol" #   qualified_name (case-insensitive endswith)
    traversal_pattern: callers_of # one of callers_of|callees_of|imports_of|
                                  # importers_of|tests_for|inheritors_of|children_of
    expected_neighbor_names:      # required, list of bare names that should
      - "expected_one"            #   appear in the traversal result
    k: 10                         # optional, top-K depth for the search step

Build stats

RepoNodesEdgesFlowsCommunitiesEmbeddingsFTS idx rows
fastapi6,29232,081165855,164127
express1,91218,877471,77147
gin1,58917,237114411,49129
code-review-graph1,4188,877104111,32638
flask1,4158,25978131,32935
httpx1,2618,22812851,19334

Embeddings count is lower than node count because File nodes aren't embedded. FTS idx rows are far lower than node count because FTS5 stores inverted-index segments, not one row per indexed document.

Agent baseline benchmark (code_review_graph/eval/benchmarks/agent_baseline.py)

The whole-corpus baseline in the standalone token benchmark is an upper bound no real agent pays. This benchmark simulates what an agent actually does without the graph:

  1. Derive search terms from each question in the config's agent_questions: list (identifier-shaped tokens via search.extract_query_identifiers, plus plain keywords; falls back to the search_queries query strings when absent).
  2. Pure-python grep over the corpus (no external rg/grep binary), ranking source files by total case-insensitive match count (deterministic; ties break on path).
  3. Read the top-3 files and token-count them (chars/4) as baseline_tokens.
  4. Compare against the graph-query cost for the same question (5 hybrid search hits + up to 5 neighbor edges per hit — the same accounting as the standalone benchmark).

Output: evaluate/results/<repo>_agent_baseline_<date>.csv with a baseline_to_graph_ratio per question. Rows where either side is zero are marked status=no_graph_results / status=no_baseline_match and excluded from aggregates (agent_baseline.aggregate). No canonical capture exists yet; numbers will be added to the canonical block above once captured — they are not quoted before being measured.

Weekly CI run (report-only)

.github/workflows/eval.yml runs every Monday at 06:23 UTC (plus manual workflow_dispatch) against the two smallest pinned configs (httpx, flask) with the token_efficiency, impact_accuracy, and agent_baseline benchmarks. It uploads the CSVs as an artifact and writes a job-summary table. It is deliberately report-only: regressions do not fail the default branch yet.

Which benchmark measures what

There are four different "token" benchmarks in the repo. They are all valid but measure different scenarios:

BenchmarkNaive baselineGraph costQuestion answered
code_review_graph/eval/benchmarks/token_efficiency.pysum of changed-file content for a specific commitfull get_review_context() JSON"Is the graph cheaper than just reading the diffed files?"
code_review_graph/eval/benchmarks/agent_baseline.pygrep top-3 files for the question's identifiers5 search hits + 5 neighbor edges per question"Is the graph cheaper than a realistic grep-and-read agent?"
code_review_graph/eval/token_benchmark.pynone — absolute per-workflow costsum of 5 MCP-tool responses"How many tokens does a complete agent workflow cost?"
code_review_graph/token_benchmark.py (standalone)sum of all source files in repo5 search hits + 5 neighbor edges per question"Is the graph cheaper than reading the whole repo?"

The code_review_graph/eval/benchmarks/token_efficiency.py numbers can be less than 1.0× for small commits (get_review_context carries impact-radius metadata and source snippets, which outweigh a tiny changed-file set). The standalone benchmark numbers are always large because the baseline is the entire repo — that is why the README leads with the median (~82×) and treats 528× as the max, and why agent_baseline exists as the realistic middle ground. Pick the one that matches the scenario you're talking about.

Generating diagrams

The 9 diagrams in diagrams/ are produced from diagrams/generate_diagrams.py. Excalidraw source files (.excalidraw) are gitignored (*.excalidraw line in .gitignore); only the rendered PNGs are tracked. Regenerate after a benchmark refresh:

uv run python diagrams/generate_diagrams.py
# Open each .excalidraw at https://excalidraw.com to render/export

Troubleshooting

git clone failed — Network or upstream rate-limit. The fix is a clean retry; the eval doesn't auto-retry by design (loud failures > silent fallback).

git checkout <sha> failed — Upstream rewrote history or removed the SHA. File an issue with the failing config so we can re-pin.

No embeddings found in this graph warning during the standalone benchmark — you skipped Step 3. Run it.

Different community IDs between runs — Make sure you're on the seeded communities.py. Check grep _LEIDEN_SEED code_review_graph/communities.py. You can override the seed via CRG_LEIDEN_SEED=<int> but all collaborators must agree on the same value.

Different naive_corpus_tokens than the canonical table — Make sure git rev-parse HEAD inside each evaluate/test_repos/<name> matches the commit: field in the corresponding config file. If not, delete the clone and let Step 2 re-clone at the pinned SHA.