DEV Community

Likitha Gujari
Likitha Gujari

Posted on

Codegraph

How I Built CodeGraph: A Living Knowledge Graph That Tells You What Breaks Before You Break It
Built for HACKHAZARDS '26 — powered by Neo4j AuraDB, tree-sitter, Groq LLaMA, and Next.js

The Problem That Frustrated Me
Every developer knows this feeling.
You join a new codebase. There are 50,000 lines of code. Your manager says "just fix this small bug in the authentication module." You make the change. You push. And suddenly three completely unrelated features are broken — a payment flow, a notification system, and a dashboard widget you've never even looked at.
You spend the next four hours tracing function calls manually, reading code you've never seen, trying to understand why changing one function in auth.py broke something in notifications.py on the other side of the codebase.
This is not a rare experience. According to JetBrains' developer survey, engineers spend 58% of their time reading and understanding code — not writing it. One wrong change in a large codebase can cost hours of debugging, failed deployments, and frustrated users.
I built CodeGraph to solve this. Not with another AI chatbot that guesses at your code. With a real, queryable knowledge graph that actually understands how your codebase is connected.

What CodeGraph Does
CodeGraph takes any public GitHub repository URL and within seconds:

Parses every function in the codebase using tree-sitter
Maps every call relationship between functions as a directed graph
Stores everything in Neo4j AuraDB as a live knowledge graph
Lets you ask questions in plain English — answered by AI grounded in real graph data

The result: paste a GitHub URL, see your entire codebase as an interactive graph, click any function, and instantly know what breaks if you change it.

The Tech Stack
Here's what I used and why each choice mattered:
Backend:

Python + FastAPI (REST API server)
Neo4j AuraDB (graph database — the core of everything)
tree-sitter (AST parser for Python, JS, TS, TSX)
Groq API with LLaMA 3.3 70B (free-tier LLM)
GitPython (clones repos at runtime)

Frontend:

Next.js 15 with TypeScript
Tailwind CSS (dark theme, purple/teal palette)
Cytoscape.js (interactive graph visualization)
Framer Motion (animations)
D3.js (rotating globe on landing page)

Why Neo4j AuraDB — Not PostgreSQL, Not MongoDB
This is the most important technical decision I made, and it's worth explaining in detail.
A codebase is inherently a graph. Functions call other functions, which call other functions, which call other functions. Understanding the consequences of a change requires traversing this graph — sometimes 5 levels deep.
In a relational database, answering "what depends on this function, directly or indirectly, up to 5 levels deep?" requires recursive JOINs that are expensive, complex to write, and slow on large datasets.
In Neo4j, it's a single Cypher query:
cypherMATCH path = (caller:Function)-[:CALLS*1..5]->(target:Function {name: $name})
RETURN caller.name, caller.file, length(path) AS depth
ORDER BY depth
That query runs in milliseconds even on a codebase with 495,000 call relationships. That's not possible with SQL.
My Neo4j schema:
cypher// Every function is a node
(f:Function {name, file, line, version})

// Every call between functions is a relationship
(caller:Function)-[:CALLS]->(callee:Function)
Simple schema. Infinite analytical power.

How I Built It — Step by Step
Step 1: The Parser
The first challenge was reading code without executing it. I used tree-sitter, a fast, error-tolerant AST (Abstract Syntax Tree) parser that works on any programming language.
For every file in a repository, tree-sitter builds a syntax tree. I walk that tree and extract:

Every function definition (name + file + line number)
Every function call (caller → callee)
Every import statement

pythondef walk(node, current_function=None):
if node.type == "function_definition":
name_node = node.child_by_field_name("name")
func_name = source_code[name_node.start_byte:name_node.end_byte]
result["functions"].append({
"name": func_name,
"file": file_path,
"line": node.start_point[0] + 1
})

if node.type == "call":
    func_node = node.child_by_field_name("function")
    called_name = source_code[func_node.start_byte:func_node.end_byte]
    if current_function:
        result["calls"].append({
            "caller": current_function,
            "callee": called_name.split(".")[-1]
        })
Enter fullscreen mode Exit fullscreen mode

One important detail: I filter out Python built-ins (print, strip, len, etc.) before storing them in Neo4j. Otherwise print would appear as the most-called function in every codebase, which is meaningless noise.
Step 2: Storing the Graph in Neo4j AuraDB
Once the parser outputs its dictionary of functions and calls, I write everything to Neo4j using MERGE statements (which prevent duplicates if the same repo is ingested twice):
pythondef store_function(name, file_path, line_number, version="default"):
with driver.session() as session:
session.run("""
MERGE (f:Function {name: $name, file: $file, version: $version})
SET f.line = $line
""", name=name, file=file_path, line=line_number, version=version)

def store_call(caller_name, callee_name):
with driver.session() as session:
session.run("""
MERGE (a:Function {name: $caller})
MERGE (b:Function {name: $callee})
MERGE (a)-[:CALLS]->(b)
""", caller=caller_name, callee=callee_name)
For the ReplyIQ repository (my own project), this produced 628 function nodes and 495,000+ call relationships — ingested in under 30 seconds.
Step 3: The Blast Radius Score
I wanted a single number that tells a developer how risky a function is to change. I called it the Blast Radius Score (0-100).
The formula:
raw = (direct_callers × 3) + (2-3 hop callers × 1) + (4-5 hop callers × 0.3)
score = min(100, round(raw × 5))
Direct callers are weighted 3x more than indirect callers because they break immediately. Functions 4-5 hops away are weighted less because the impact is more speculative.
A score of 100 means "change this and half your codebase breaks." A score of 0 means "nobody calls this — probably safe to delete."
Step 4: GraphRAG — AI Grounded in the Graph
This is where it got interesting.
Regular RAG (Retrieval Augmented Generation) retrieves text chunks from a vector database and passes them to an LLM. I did something different: GraphRAG — the AI agent retrieves structured graph data from Neo4j and reasons over it.
I gave the Groq LLaMA model a set of tools it can call:
pythonTOOLS_SCHEMA = [
{
"type": "function",
"function": {
"name": "get_impact",
"description": "Find what breaks if a function is changed",
"parameters": {
"type": "object",
"properties": {
"function_name": {"type": "string"}
},
"required": ["function_name"]
}
}
},
# ... get_stats, find_dead_code, get_blast_radius, run_cypher
]
When a user asks "What is the riskiest function to change?", the model automatically calls get_blast_radius, reads the top result from Neo4j, and answers with the actual function name, score, and file path. No hallucination — because the answer comes from the database, not from the model's training data.
Step 5: The Frontend
The dashboard has three main sections:
Left panel: AI chat powered by the Groq agent. Type any question, get a grounded answer with specific function names and file paths.
Right panel: Cytoscape.js interactive graph. Nodes are colored by blast radius score — red (critical), amber (moderate), yellow (low), grey (healthy). Click any node and an impact drawer slides in showing the full CRITICAL/MODERATE/LOW dependency chain.
Top bar: GitHub URL input. Paste any public repo URL and the entire pipeline runs automatically.
The landing page features an animated rotating wireframe globe built with D3.js, floating geometric shapes from Framer Motion, and a scroll-driven reveal section — all themed in the dark purple/teal CodeGraph palette.

Challenges I Faced
Challenge 1: tree-sitter version incompatibility
The first version I installed (tree-sitter==0.21.3) had a breaking API change where Language() expected different arguments depending on the exact patch version. After two hours of debugging, I discovered the fix was to upgrade to tree-sitter==0.25.2 with matching tree-sitter-python==0.25.0. The lesson: always pin both the core library and language bindings to the same major version.
Challenge 2: Gemini API keys that don't work
I originally planned to use Google Gemini for the AI agent. After hours of debugging, I discovered that new Google accounts (since mid-2026) are issued AQ.-prefix API keys that are incompatible with the standard Gemini Developer API — they require OAuth flows instead of simple API key authentication. Switching to Groq (completely free, genuinely no billing required) solved this instantly and actually gave better tool-calling reliability.
Challenge 3: Python built-ins polluting the graph
When I first ran the parser on Flask, print and strip showed up as the highest-risk functions with blast radius scores of 25+. This is because they appear as "called" functions throughout the codebase. I fixed this by maintaining a filter list of ~80 Python built-in names and skipping them during store_call(). The graph immediately became meaningful.
Challenge 4: Cytoscape edge validation
When the graph first rendered, it crashed with: Cannot create edge with nonexistent target 'jsonify'. Cytoscape requires both the source and target of every edge to exist as nodes before you can create the edge. The fix was simple — build a Set of valid node IDs and filter edges against it before passing them to Cytoscape.
Challenge 5: Neo4j Aura auto-pausing
Neo4j Aura's free tier automatically pauses instances after inactivity. During development, this caused confusing getaddrinfo failed DNS errors that looked like network problems. The fix: always check console.neo4j.io before a demo session and click Resume if the instance is paused. I also added better error messages in the FastAPI routes to return a helpful 503 response instead of a generic 500.

The Results
Tested on three real codebases:
RepositoryFunctionsRelationshipsIngest TimeReplyIQ (my own project)628495,000+~25 secondsFlask (pallets/flask)944832~18 secondsemotion_aitutor12431,744~8 seconds
The AI correctly identified useRoom as the riskiest function in ReplyIQ (blast radius 100/100, 9 direct callers). The impact drawer showed exactly which components would break — HTMLOverlay, NavigationUI, CameraController, and 6 others — all with correct file paths, verified against manual code review.

What I'd Build Next
VS Code extension — inline blast radius warnings as you type. Hover over a function and see its risk score without leaving your editor.
GitHub Actions integration — automatically run impact analysis on every PR. If a PR touches a function with blast radius > 80, the CI pipeline adds a warning comment listing what might break.
JavaScript/TypeScript parser — tree-sitter supports every major language. Adding JS/TS parsing is the highest-leverage next step.
Team collaboration — shared graph databases so entire engineering teams can query the same codebase graph simultaneously.

Try It Yourself

Live demo: [https://codegraph-nine.vercel.app/]
GitHub: [https://github.com/likitha2003-ctrl/codegraph]
API docs: [https://codegraph-backend-u0qq.onrender.com]

To run locally:
bashgit clone https://github.com/likitha2003-ctrl/codegraph.git
cd codegraph/backend
cp .env.example .env

Fill in your Neo4j and Groq keys

pip install -r requirements.txt
uvicorn main:app --reload --port 8000
bashcd codegraph/frontend
echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local
npm install && npm run dev
Then open http://localhost:3000 and paste any public GitHub URL.

Final Thoughts
The most important insight from building CodeGraph: the right data structure makes hard problems easy.
Multi-hop dependency analysis sounds complex. In Neo4j, it's one line of Cypher. Blast radius scoring sounds like it needs ML. It's arithmetic over a graph query result. Dead code detection sounds like static analysis. It's a WHERE NOT EXISTS query.
The graph database didn't just store our data — it made our product possible. That's the difference between choosing a database because it's familiar and choosing one because it's the right tool for the specific shape of your problem.
If you're building anything where relationships between entities matter — social networks, dependency trees, supply chains, knowledge graphs — reach for Neo4j before you reach for Postgres. You'll thank yourself later.

Built with ❤️ for HACKHAZARDS '26
Tags: #neo4j #graphdatabase #auradb #hackathon #developertools #python #nextjs #ai #llm #graphrag #treesitter #groq #hackhazards

Top comments (0)