Structured codebase context for LLMs. Graphite turns JVM bytecode into a queryable program graph — so AI agents can understand your codebase without reading every file.
LLMs working with code face a fundamental constraint: context windows are finite, but codebases are not.
Dumping source files into a prompt is wasteful. Most tokens describe boilerplate, imports, and formatting — not the relationships that matter. An LLM trying to understand "what calls this method?" or "what constants flow into this API?" must read hundreds of files to answer questions that a graph can answer in milliseconds.
Graphite builds a program graph from compiled bytecode — nodes are program elements (methods, fields, constants, call sites), edges are relationships (dataflow, calls, type hierarchy). LLMs query the graph instead of reading source code.
Before Graphite: Feed 500 source files (~2M tokens) to find AB test IDs.
With Graphite: Query graph.callSites(pattern) → get 23 constants in 12 tokens.
| Relationship | Example | LLM Use Case |
|---|---|---|
| Dataflow | x = 42; foo(x) → constant 42 flows to foo | Track config values, feature flags, API keys |
| Call graph | UserService.save() calls Repository.insert() | Understand execution paths without reading source |
| Type hierarchy | AdminUser extends User implements Auditable | Resolve polymorphism, find implementations |
| Annotations | @GetMapping("/api/users") on listUsers() | Discover endpoints, serialization rules, DI config |
| Lambda/method ref | items.stream().map(User::getName) | Trace functional pipelines |
| Resources | config/application.yml inside a fat JAR | Cross-reference code with config files |
| Task | Raw Source | Graphite Query | Reduction |
|---|---|---|---|
| Find all AB test IDs | ~500 files, 2M tokens | callSites + backwardSlice → 23 results | 99.99% |
| Map REST endpoints | ~200 controllers, 800K tokens | memberAnnotations scan → structured list | 99.9% |
| Find dead code | Entire codebase, 5M tokens | branchScopes + callSites → dead paths | 99.99% |
| Resolve type hierarchy | ~100 files per type chain | supertypes / subtypes → direct answer | 99% |
Graphite uses Cypher (the industry-standard graph query language) for querying. The Cypher engine is in the graphite-cypher module, powered by an ANTLR-based openCypher parser.
Tools like GitNexus, Aider, and most LLM code assistants use Tree-sitter for codebase understanding. Tree-sitter parses syntax — it sees text structure, not program semantics.
| Capability | Tree-sitter | Graphite |
|---|---|---|
| "What type is this variable?" | No — sees var x = foo(), can't resolve foo's return type | Yes — full type resolution from bytecode |
| "What values flow into this parameter?" | No — can't cross method boundaries | Yes — inter-procedural backward slice |
| "Does this interface have implementations?" | Heuristic grep for class names | Yes — complete type hierarchy from class metadata |
| "What does this lambda actually call?" | No — invokedynamic is invisible in source | Yes — MethodHandle extraction from bootstrap args |
| "Is this field used via reflection/DI?" | No — annotation semantics are opaque | Yes — annotation values are queryable data |
"What's the real type of Object fields?" | No — requires dataflow across methods | Yes — cross-method field assignment tracking |
| Controller inheritance | No — can't resolve inherited annotations | Yes — walks type hierarchy for endpoint discovery |
The fundamental issue: Tree-sitter operates on syntax (one file at a time, no type resolution, no cross-file dataflow). Graphite operates on semantics (compiled bytecode with full type information, inter-procedural analysis, resolved generics).
For LLMs, this difference is critical. A syntax tree tells you what code looks like. A program graph tells you what code does.
# Install via Homebrew
brew tap johnsonlee/tap
brew install graphite
# Build a graph from your JAR
graphite build app.jar -o /data/app-graph --include com.example
# Build a graph from an Android APK
graphite build app.apk \
-o /data/apk-graph \
--include com.example
# Query with Cypher
graphite query /data/app-graph \
"MATCH (c:IntConstant)-[:DATAFLOW*]->(cs:CallSiteNode) WHERE cs.callee_class =~ 'com.example.*' RETURN c.value, cs.callee_name"# JSON output (for LLM consumption)
graphite query --format json /data/app-graph \
"MATCH (n:CallSiteNode) RETURN n.callee_name LIMIT 10"# Launch the web UI
graphite serve --id app /data/app-graph --port 8080
# Serve multiple graphs by id. Relative graph paths resolve under --data.
graphite serve --data /data/graphs \
--graph orders:orders-graph \
--graph billing:/data/billing-graph \
--port 8080
# Hot-load or replace a graph without restarting the server
curl -X PUT http://localhost:8080/api/graphs/orders \
-H 'Content-Type: application/json' \
-d '{"path":"/data/graphs/orders-graph-v2"}'For APK inputs, Graphite uses Android platform jars to resolve the APK's target
API level. Pass --android-sdk with the Android SDK root. If omitted,
Graphite searches in this order:
ANDROID_HOME, thenANDROID_SDK_ROOT.- Default SDK roots for the current OS:
- macOS:
~/Library/Android/sdk,/opt/homebrew/share/android-commandlinetools,/usr/local/share/android-commandlinetools - Linux:
~/Android/Sdk,~/android-sdk,/opt/android-sdk,/usr/local/android-sdk,/usr/lib/android-sdk - Windows:
%USERPROFILE%\AppData\Local\Android\Sdk
- macOS:
- SDK roots inferred from
adb,emulator, orsdkmanageronPATH.
// Build graph from bytecodeval graph =JavaProjectLoader(LoaderConfig(
includePackages =listOf("com.example")
)).load(Path.of("/path/to/app.jar"))
// Cypher queryval result = graph.query(""" MATCH (c:IntConstant)-[:DATAFLOW*]->(cs:CallSiteNode) WHERE cs.callee_class =~ 'com.example.*' RETURN c.value, cs.callee_name""")
result.rows.forEach { row ->println("${row["c.value"]} -> ${row["cs.callee_name"]}")
}
// Programmatic query DSLval results =Graphite.from(graph).query {
findArgumentConstants {
method {
declaringClass ="com.example.ab.AbClient"
name ="getOption"
}
argumentIndex =0
}
}
// Annotations, dataflow analysisval annotations = graph.memberAnnotations("com.example.User", "name")
val slice =DataFlowAnalysis(graph).backwardSlice(nodeId)
slice.constants() // all constant values that reach this node// Save to disk (WebGraph compressed format)GraphStore.save(graph, Path.of("/data/app-graph"))
// Load — auto-adaptive based on graph size:// < 1M nodes → eager (all in heap, fastest queries)// >= 1M nodes → mmap (nodes off heap, 75% less memory)val graph =GraphStore.load(Path.of("/data/app-graph"))
// Or force a specific strategyval graph =GraphStore.load(dir, GraphStore.LoadMode.EAGER) // always in-heapval graph =GraphStore.load(dir, GraphStore.LoadMode.MAPPED) // always mmapgraph.resources.list("**/*.xml").forEach { entry ->println(entry.path) // e.g., "config/application.yml"
}Resources are also indexed into the graph, so you can query them with Cypher and cross-reference them with call sites:
// Structured resource valuesMATCH (r:ResourceValue{key:"feature.mode"})
RETURNr.path, r.value// Nested JSON / XML valuesMATCH (r:ResourceValue)
WHEREr.keyIN ["feature.enabled", "service.endpoint", "service.@enabled"]
RETURNr.path, r.key, r.value// Which call sites read a specific keyMATCH (r:ResourceValue{key:"feature.mode"})-[:RESOURCE_LOOKUP]->(cs:CallSiteNode)
RETURNcs.caller_signature, cs.callee_signature// Resource files opened by codeMATCH (f:ResourceFile)-[e:RESOURCE_OPEN|RESOURCE_LOAD|RESOURCE_BUNDLE_CANDIDATE]->(cs:CallSiteNode)
RETURNf.path, e.kind, cs.caller_signature, cs.callee_signatureResource relationships are exposed as dedicated edge types:
| Type | Meaning |
|---|---|
RESOURCE_CONTAINS | ResourceFile -> ResourceValue |
RESOURCE_OPEN | Resource file opened directly by code |
RESOURCE_LOAD | Resource content loaded by parsers/bundles |
RESOURCE_BUNDLE_CANDIDATE | ResourceBundle.getBundle(...) candidate resolution |
RESOURCE_LOOKUP | Concrete key/value lookup (getProperty, getString, getObject) |
RESOURCE_KEYS | Key enumeration (getKeys) |
Resource path indexing currently covers:
.properties.yml/.yaml- Java properties XML (
Properties.loadFromXML) .json- generic
.xml ListResourceBundle/ provider-backed class bundles via path-level class indexing
Generic JDK resource linking currently covers:
ClassLoader.getResource*Properties.load(...)Properties.loadFromXML(...)PropertyResourceBundle(...)ResourceBundle.getString/getObject/getKeysResourceBundle.getBundle(...)with locale-aware candidate resolution- common
ResourceBundle.Controlcases includingFORMAT_*, no-fallback controls, and simple customgetFormats/getCandidateLocalesoverrides
graphite serve exposes resource-aware HTTP APIs for agents and tooling:
| Endpoint | Description |
|---|---|
/api/graphs | List loaded webgraphs with cached per-graph statistics and aggregate totals |
/api/graphs/{graphId} | Get, load, replace, or unload a webgraph by id |
/api/graphs/{graphId}/... | Query one explicit webgraph with the direct single-graph response shape |
/api/cypher | Run one Cypher query over the union of every loaded graph |
/api/cypher/graphs | Run one query over an explicit graph set, or explicitly fan out per graph |
/api/nodes, /api/methods, ... | Query every loaded graph; non-Cypher results are grouped by graphId |
/api/resources | List indexed resources in every graph, grouped by graphId |
/api/resources/{path} | Read every matching resource without path collisions, grouped by graphId |
/api/endpoints | Extract framework HTTP endpoints from every graph, grouped by graphId |
/openapi.json | Machine-readable OpenAPI document for the explore server |
/swagger.json | Swagger-compatible alias of the same API document |
Graph-local node IDs are accepted only by graph-scoped routes such as
/api/graphs/{graphId}/node/{id} and
/api/graphs/{graphId}/subgraph?center={id}. The corresponding root routes do
not exist because the same local ID can identify unrelated nodes in different
graphs.
There is no default graph and no automatic graph selection. Root graph APIs
always mean all loaded graphs; /api/graphs/{graphId}/... always means exactly
one graph. Every root non-Cypher result is grouped by graphId, while every
cross-graph Cypher row includes $metadata.graphIds and returned graph elements include
qualified identities such as elementId = "orders:42".
For agent-driven discovery, probe /openapi.json first. It describes the full
root-all and graph-scoped REST surface, including the two explicit modes of
/api/cypher/graphs.
graphite/
├── graphite-core/ # Graph interface, nodes, edges, analysis
├── graphite-cypher/ # Cypher query engine (ANTLR parser + executor)
├── graphite-sootup/ # SootUp bytecode → graph builder
├── graphite-webgraph/ # WebGraph disk persistence (BVGraph + LAW tools)
├── graphite-query/ # CLI: build, query, serve
└── graphite-explore/ # Explore HTTP routes and legacy standalone launcher
Graphs are persisted using the WebGraph ecosystem:
| Data | Format |
|---|---|
| Adjacency | BVGraph (2-4 bits/edge) |
| Edge labels | Byte array in BVGraph order |
| Strings | FrontCodedStringList (prefix compression) |
| Node data | Compact binary with string table indices |
| Metadata | Compact binary with string table indices |
| Capability | Description |
|---|---|
| Constant tracking | Direct, local variable, field, cross-class, enum |
| Auto-boxing | Integer.valueOf() transparent handling |
| Lambda / method ref | invokedynamic → actual target resolution |
| Functional dispatch | Callbacks, return values, fields, varargs, conditionals |
| Controller inheritance | Endpoint discovery follows class hierarchy |
| Generic type analysis | ApiResponse<PageData<User>> nested structure |
| Branch reachability | Dead code via condition constant analysis |
| Annotations | Generic memberAnnotations() for any framework |
| Cypher queries | graph.query("MATCH ...") -- full openCypher read grammar |
| Resource access | Files inside JAR/WAR/fat JAR (nested JARs) |
Pluggable via GraphiteExtension SPI (ServiceLoader):
classMyExtension : GraphiteExtension {
overridefunvisit(sootClass:SootClass, context:GraphiteContext) {
// Extract domain-specific metadata during graph building
context.addMemberAnnotation(className, memberName, annotationFqn, values)
}
}Register in META-INF/services/io.johnsonlee.graphite.sootup.GraphiteExtension.
repositories {
mavenCentral()
}
dependencies {
implementation("io.johnsonlee.graphite:core:2.1.0")
implementation("io.johnsonlee.graphite:sootup:2.1.0")
// Optional: Cypher query support (graph.query("MATCH ..."))
implementation("io.johnsonlee.graphite:cypher:2.1.0")
// Optional: disk persistence (WebGraph format)
implementation("io.johnsonlee.graphite:webgraph:2.1.0")
}Connect LLMs to Graphite via Model Context Protocol:
npx graphite-mcpConfigure in Claude Code (~/.claude/settings.json):
{
"mcpServers": {
"graphite": {
"command": "npx",
"args": ["graphite-mcp"],
"env": { "GRAPHITE_URL": "http://localhost:8080" }
}
}
}Start the Explorer first, then LLMs can query the graph:
# Start Explorer
graphite serve --id app /path/to/saved-graph
# The serve command defaults to --load-mode MAPPED for multi-graph heap stability.You can also start with no initial graph and hot-load services later:
graphite serve --data /data/graphs
curl -X PUT http://localhost:8080/api/graphs/orders \
-H 'Content-Type: application/json' \
-d '{"path":"orders-graph"}'Graph replacement is atomic for readers. Requests that already acquired the previous graph finish against that snapshot, requests acquired after the swap use the replacement, and the previous graph is closed only after its last request releases it. A replacement that fails to load leaves the current graph unchanged.
To run one query across an explicit graph set:
curl -X POST http://localhost:8080/api/cypher/graphs \
-H 'Content-Type: application/json' \
-d '{"query":"MATCH (n:IntConstant) RETURN n.value","graphs":["orders","billing"],"limit":100}'The default mode is cross-graph: patterns, joins, filters, and aggregations
operate once over the selected graph union. Every row reports all contributing
graphs in $metadata.graphIds. To preserve independent per-graph execution, explicitly
send "mode":"fanout"; only this mode accepts perGraphLimit and
includeGraphRows. In both modes, limit caps the total response row count.
The MCP tools follow the same rule: omitting graph_id queries all graphs;
providing graph_id selects exactly one graph. The cypher tool can also use
graphs: ["orders", "billing"] for an explicit subset or all_graphs: true
with mode: "cross-graph" or mode: "fanout".
LLMs can now use tools such as openapi, graphs, cypher, resources, resource, endpoints, c4, nodes, methods, call_sites, and annotations.
The explore server also exposes a single C4 architecture endpoint:
GET /api/architecture/c4?level=context|container|component|all
GET /api/architecture/c4?level=context|container|component|all&format=dsl
GET /api/architecture/c4?level=context|container|component|all&format=mermaid
GET /api/architecture/c4?level=context|container|component|all&format=plantuml
Agents can use it to retrieve code graph-derived C4 architecture views without
guessing multiple endpoints. The default response is a Structurizr workspace
JSON document. For text rendering, use format=dsl, format=mermaid, or
format=plantuml.
Copyright 2026 Johnson Lee
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0