OneGraph 以真實依賴圖為 Gemma 4 的程式碼審查提供依據
OneGraph: grounding Gemma 4's code reviews in a real dependency graph instead of a guess
OneGraph 將 Graphify 抽取的真實依賴圖接入 PR-Agent 的 /graph_risk 指令,按影響範圍計算風險,再由 Gemma 4 撰寫説明。針對同一問題的 A/B 測試中,兩種方案對所引用檔案的引用準確率均為 100%;依賴圖方案找到並引用更多相關呼叫點。
OneGraph: grounding Gemma 4's code reviews in a real dependency graph instead of a guess
An LLM code review tool that reads a diff in isolation has a blind spot: it cannot see what else in the repository depends on the lines that changed. It can comment on the diff, but it cannot tell you the function you just touched is called from nineteen other places, three of them in a different service. That is not a hallucination in the usual sense, it is a coverage gap, and it is the gap code-review-graph and the knowledge-graph-over-vector-search approach in this post about replacing grep-based review both set out to close, each in their own way.
OneGraph is my attempt at the same problem, built for Hacktoberfest Hack Day Pune x GDG on Campus VU. It pairs Graphify, a tree-sitter based dependency graph extractor, with Gemma 4, and wires the result directly into PR-Agent as a live /graph_risk command. There's also a related post worth reading if you want the extraction side in more depth: Graphify: AST-Based Knowledge Graphs for Agent Code Navigation.
Repo: https://github.com/CodersAcademy006/onegraph
Live demos: https://graphs-site-omega.vercel.app/
Live in a real PR: https://github.com/CodersAcademy006/onegraph-flask-demo/pull/1
The split that makes this work
Everything in OneGraph follows one rule: code owns the facts, Gemma owns the words.
-
Graphify extracts.
graphify extract --code-onlyruns tree-sitter over the repo and builds every call, import and containment edge into a real graph. No LLM call, no API key, $0 to build, deterministic. -
Graphify labels.
graphify labelruns community detection (Leiden/Louvain) and names every cluster. - Gemma 4 narrates. It reads the graph traversal result, not the raw source, and writes the plain-English explanation. It never computes risk, it never counts dependents, it only describes what the graph already proved.
-
PR-Agent posts it. A
/graph_riskcomment on any pull request, with a risk level computed from a real blast-radius count and a link to the full interactive graph.
The risk level is arithmetic, not a model's opinion of its own severity. A self-rated "this feels risky" from an LLM is noise; a count of how many real call sites a changed function has is a fact.
What it looks like on a real PR
I ran this against a real demo PR, not a constructed example, and verified the result through the GitHub API before trusting it:
The comment text comes from a small f-string template in pr_graph_risk.py:
comment = (
f"**Risk: {level}**, full repo graph: **{node_count:,} nodes / {edge_count:,} edges** "
f"({real_time_ms}ms, $0 LLM cost to build)\n\n"
f"Affects: {affects}\n\n"
f"{gemma_analysis}\n\n"
f"Graphify's own viewer: force layout, community clusters, click any node.\n"
f"{graph_url}"
)
Everything numeric in that string comes from graph.json, computed before Gemma ever sees a prompt. Gemma only fills in gemma_analysis.
Wiring into an existing tool, not rebuilding it
PR-Agent already has a tool registry. Adding /graph_risk is two lines:
# pr_agent/tools/pr_graph_risk.py registers itself here
command2class = {
...,
"graph_risk": PRGraphRisk,
}
I did the same thing for deepwiki-open: one environment variable, ONEGRAPH_BACKEND=graphify, gates research_chat()'s context source. Unset, the original embedding-RAG path runs untouched. Both diffs are in the repo under integrations/, each under 40 lines against the upstream file.
Real scale, not a toy demo
I ran Graphify against four public repos end to end, nothing trimmed, nothing simulated. Every number below is read directly from the extracted graph.json:
| Repo | Nodes | Edges | Communities |
|---|---|---|---|
| pallets/flask | 2,151 | 3,569 | n/a |
| NVIDIA/Megatron-LM | 48,295 | 133,164 | 1,275 |
| Graphify-Labs/graphify (the sponsor tool, graphing itself) | 18,552 | 42,018 | 865 |
| supabase/supabase | 65,361 | 247,294 | 1,400 |
All four are live, clickable, force-directed graphs, not screenshots. Supabase's 65k-node graph loads and stays interactive because Graphify's exporter aggregates above a node-limit into community clusters for rendering, while graph_stats() still reports the true full count underneath.
The honest finding from the A/B test
I ran the same question against the project both with and without the graph as context, to see whether grounding actually changes anything. The tempting headline would be "zero hallucinated citations," but that is not what the data showed: the embedding-RAG baseline did not hallucinate either, both runs were 100% citation-accurate on the files they cited.
The real difference was coverage. The graph-grounded answer found and cited more of the actually-relevant call sites, because it could traverse real edges instead of ranking chunks by embedding distance. This matches what the vector-search-replacement post above found independently: vector search returns what is semantically similar, a graph traversal returns what is actually connected. Those are not the same question, and for a dependency question, only one of them is the right one.
What it actually costs to run
Instead of estimating a token price, I measured one real Gemma 4 call end to end, served locally through Ollama (gemma4:12b-it-q4_K_M), no API key, no cloud bill:
wall time: 132.8s
prompt_eval_count (input tokens): 2,438
eval_count (output tokens): 1,658
context sent: 6,697 characters
cost: $0
That is the real number for one /graph_risk run on a local machine, not a projection.
A bug the subprocess wrapper hid
core.py wraps the graphify CLI with subprocess.run(..., cwd=str(path)). Early on I passed a relative Path into export_graph_html() and got a bare CalledProcessError with nothing useful in stderr. The cause: a relative path gets re-resolved against the changed cwd inside the subprocess, so the graph-json argument silently points somewhere that does not exist. Running the underlying graphify export html command directly, outside the wrapper, worked fine and gave it away. The fix is one .resolve() call at every site that calls into the wrapper. It is a small bug, but it is the kind that looks like a Graphify problem until you actually trace where the path gets rewritten.
Try it on your own repo
./onegraph <github-url> [slug]
Clones, extracts, labels, exports, deploys, and prints the live URL. For a judge who wants to run it on their own repository right there during review.
Why open source mattered for this build
Graphify's extraction is $0 because it never calls an LLM to understand structure, tree-sitter already knows the grammar. Gemma running locally through Ollama means the narration step also costs nothing per call, which matters for a tool meant to run on every PR, not just a demo. And because PR-Agent and deepwiki-open are both open, the two-line and one-env-var integration points above are things anyone can verify by reading the actual diff, not something I am asking you to take on faith.
Full README, architecture, and every screenshot above: https://github.com/CodersAcademy006/onegraph
來源:DEV Community · dev.to


