Important
graphlens is archived and no longer maintained. Its successor is callix.
callix is a full rewrite of this project by the same author, with the analysis moved to Rust. What it changes:
- One install, no assembly. graphlens asks you to bring the language
servers yourself β
ty,gopls,rust-analyzer,intelephenseβ and to keep them onPATHand in step with each other. callix links the Python and TypeScript type checkers into the module, standard-library stubs included, sopip install callixis the whole setup. Go and Rust still use the toolchain your project already has, but no separate language server. - Faster. 5.2Γ on apache/superset, 3.3Γ on colinhacks/zod, 2.6Γ on gin-gonic/gin. The resolution phase in particular, which used to be JSON-RPC round-trips, dropped from 39.1s to 8.9s on superset.
- Lighter. Resolving Rust no longer keeps an interactive
rust-analyzerserver resident β a batch SCIP index is written once and read statically, instead of a process that grows into tens of gigabytes on a large workspace. - Same graph. The same 14 node kinds, 12 relation kinds, deterministic IDs and serialization format: a graph written here reads there and back. Structural parity is verified by diffing both implementations on superset, zod, gin, ripgrep and ruff.
Not carried over: the CLI, the MCP server, the Neo4j backend, the HTML visualization, and the PHP and C# adapters. If you depend on 10BC0 those, this repository stays readable at its final state.
β Migration notes
Extensible polyglot code analysis framework that parses source projects, normalizes their structure into a shared graph IR, and exposes it for dependency analysis, navigation, and code intelligence tooling.
Documentation Β· Repository Β· Issues
Repository β Language Adapter β GraphLens (IR) β Graph Backend
| Layer | Responsibility |
|---|---|
| Language Adapter | Parses source files, produces GraphLens |
| GraphLens | Typed nodes + directed relations (the IR) |
| Graph Backend | Persists or queries the graph (Neo4j, in-memory, β¦) |
Adapters are pure data producers β they never write to any backend. The graph is the only output.
- Language-agnostic β one shared model for Python, TypeScript, Go, Rust, PHP, C#, β¦
- Plugin-based adapters β each language is a separate package, registered via Python entry points
- Tree-sitter powered β all adapters use tree-sitter for CST parsing and exact span positions, combined with type-aware resolution (ty for Python, TypeScript Compiler API for TypeScript, gopls for Go, rust-analyzer for Rust, Intelephense for PHP, scip-dotnet for C#)
- Cross-language aware β adapters emit language-agnostic
BOUNDARYports (HTTP, queues, gRPC, Temporal);graphlens-linkconnects a consumer in one language to a provider in another - Monorepo aware β
can_handle()andfind_*_roots()handle multi-language repos correctly - Deterministic node IDs β SHA-256 hash of
project::kind::qualified_nameβ stable across re-scans
Analysis throughput on large real-world projects, refreshed automatically on
every release β one cold run per project inside the published Docker image
(so the numbers reflect exactly the toolchain users get). See
benchmarks/ to reproduce locally or add a project.
Last run: 2026-07-24 12:52 UTC Β· image latest Β· runner Linux x86_64 Β· single cold run, indicative only.
| Project | Lang | Commit | LOC | Files | Nodes | Relations | Time | Peak RSS | KLOC/s | Resolver | Resolved |
|---|---|---|---|---|---|---|---|---|---|---|---|
| apache/superset | python | c83fb2b |
399 519 | 1 886 | 156 044 | 379 553 | 150.9s | 1,793 MB | 2.6 | ok | 84% of 281 667 (74s) |
| colinhacks/zod | typescript | 1fb56a5 |
74 194 | 404 | 8 623 | 24 557 | 22.2s | 567 MB | 3.3 | ok | 87% of 15 771 (19s) |
| gin-gonic/gin | go | 73726dc |
23 672 | 98 | 7 227 | 11 882 | 13.9s | 2,186 MB | 1.7 | ok | 100% of 8 920 (13s) |
| casdoor/casdoor | go | 696bcf0 |
86 898 | 458 | 14 987 | 28 276 | 130.1s | 14,503 MB | 0.7 | ok | 100% of 19 421 (127s) |
| gohugoio/hugo | go | 4d22555 |
224 821 | 897 | 34 809 | 72 225 | 110.8s | 9,499 MB | 2.0 | ok | 99% of 49 013 (104s) |
| BurntSushi/ripgrep | rust | 4649aa9 |
50 275 | 98 | 5 365 | 15 087 | 13.9s | 1,264 MB | 3.6 | ok | 99% of 11 435 (0s) |
| tokio-rs/axum | rust | c59208c |
43 653 | 296 | 8 093 | 14 798 | 70.2s | 4,544 MB | 0.6 | ok | 88% of 9 662 (0s) |
| astral-sh/ruff | rust | 6686f63 |
687 409 | 1 870 | 69 708 | 217 127 | 174.3s | 8,094 MB | 3.9 | ok | 100% of 155 276 (7s) |
| laravel/framework | php | bd8aeb6 |
441 358 | 2 478 | 139 764 | 189 708 | 113.9s | 2,958 MB | 3.9 | ok | 56% of 191 435 (98s) |
| dotnet/eShop | csharp | 9b4f943 |
25 385 | 527 | 13 124 | 29 171 | 26.0s | 2,413 MB | 1.0 | ok | 80% of 26 629 (2s) |
| microsoft/reverse-proxy | csharp | 4154b6d |
63 236 | 585 | 9 714 | 17 756 | 6.7s | 77 MB | 9.5 | unavailable | 0% of 72 295 (5s) |
| Total | 2 120 420 | 467 458 | 832.8s | 2.5 | 75% of 841 524 |
βΉοΈ prepare exited 155 (dotnet restore YARP.sln)
Peak RSS measured via cgroup.v2 (whole process tree, incl. LSP resolver subprocesses). KLOC/s = analysed thousands-of-lines per second. Generated by benchmarks/run_benchmarks.py.
Full product documentation lives at https://Neko1313.github.io/graphlens/
(built with Docusaurus from website/):
- Getting Started β install, quick start, core concepts
- Guides β library API, CLI, querying, visualization, Neo4j, cross-language
- CI Integration β strict mode, GitHub Actions, Docker, local hooks
- Adapters β Python, TypeScript, Go, Rust, PHP, C#, and writing your own
- Graph Model β nodes, relations, boundaries, serialization
- API Reference β exact signatures
To run the docs locally: cd website && pnpm install && pnpm start.
# Core library only (models, contracts, registry)
pip install graphlens
# Core + Python adapter
pip install "graphlens[python]"
# Core + TypeScript adapter
pip install "graphlens[typescript]"
# Core + Go / Rust / PHP / C# adapters
pip install "graphlens[go]"
pip install "graphlens[rust]"
pip install "graphlens[php]"
pip install "graphlens[csharp]"
# CLI (graphlens analyze / visualize / query / neo4j)
pip install "graphlens-cli[python]" # with Python adapter
pip install "graphlens-cli[all]" # Python + TS + Go + Rust + PHP + C# + Neo4jWith uv:
uv add graphlens
uv add "graphlens[python]"
uv add "graphlens[typescript]"
uv add "graphlens-cli[all]"For CI, the published image bundles the CLI with every adapter and the
toolchains their resolvers drive (ty, Node, Go + gopls, Rust + rust-analyzer,
PHP + Intelephense) β no local setup required, and the supported way to get the
Go, Rust and PHP adapters (which are not published to PyPI). Mount your project
at /workspace:
docker run --rm -v "$PWD:/workspace" ghcr.io/neko1313/graphlens \
analyze /workspace --output /workspace/graph.jsonThe image is published to the GitHub Container Registry on each release
(:latest plus :X.Y.Z / :X.Y version tags).
from pathlib import Path
from graphlens import adapter_registry
# Load and instantiate the Python adapter
adapter = adapter_registry.load("python")()
# Analyze a project β returns a GraphLens
graph = adapter.analyze(Path("./my-project"))
print(f"Nodes: {len(graph.nodes)}")
print(f"Relations: {len(graph.relations)}")
# Inspect nodes by kind
from graphlens import NodeKind
modules = [n for n in graph.nodes.values() if n.kind == NodeKind.MODULE]
classes = [n for n in graph.nodes.values() if n.kind == NodeKind.CLASS]
# Check the resolver actually ran (don't trust a silently degraded graph)
from graphlens import RESOLVER_STATUS_KEY
assert graph.metadata[RESOLVER_STATUS_KEY] == "ok"
# Query the graph (indexed lookups, no manual scanning)
fn = next(n for n in graph.nodes.values() if n.name == "my_function")
callers = graph.callers(fn.id) # who calls it
callees = graph.callees(fn.id) # what it calls
near = graph.neighbors(fn.id, depth=2) # 2-hop neighbourhood
# Serialize for pipelines / agents (round-trippable JSON), then reload
text = graph.to_json(indent=2)
graph2 = type(graph).from_json(text)
# Diff two scans (e.g. before/after a change)
diff = old_graph.diff(graph)
print(diff.added_nodes, diff.removed_relations, diff.is_empty)Install graphlens-cli to get the graphlens entry point:
# Print node/relation statistics
graphlens analyze <project_root>
graphlens analyze ~/myrepo --lang python,typescript,go,rust
# Serialize the graph to JSON (CI indexing step); --strict fails on a
# degraded resolver so a pipeline never feeds agents an incomplete graph
graphlens analyze ~/myrepo --output graph.json
graphlens analyze ~/myrepo --format json
graphlens analyze ~/myrepo --strict
# Query a saved graph (callers | callees | references | neighbors)
graphlens query my_function --graph graph.json --op callers
graphlens query MyClass.method --graph graph.json --op neighbors --depth 2
# Interactive HTML graph viewer (opens in browser)
graphlens visualize <project_root>
graphlens visualize ~/myrepo --lang python --show-external --max-nodes 500
graphlens visualize . --output graph.html --no-open
# Export to Neo4j
graphlens neo4j <project_root> --uri bolt://localhost:7687 --user neo4j --password secret
graphlens neo4j . --wipe --batch-size 200graphlens is only an analysis engine and ships no MCP server of its own. To serve the graph to coding agents (Claude Code, Cursor, β¦) over the Model Context Protocol, use graphlens-mcp β a separate, MIT-licensed server built on top of this engine (docs):
uv tool install graphlens-mcp
cd your-project && graphlens-mcp initIt is also a worked example of how to consume the engine: driving the adapter registry, persisting and refreshing the graph, and exposing it to a client.
Produces a self-contained HTML file powered by vis.js and opens it in the browser.
| Flag | Description |
|---|---|
--lang auto|python|typescript|python,typescript |
Adapters to use (default: auto-detect all) |
--show-external |
Include stdlib / third-party external symbol nodes |
--show-structure |
Add CONTAINS / DECLARES structural edges |
--max-nodes N |
Prune low-degree nodes above N (default: 1500) |
--output PATH |
Write HTML to PATH instead of graph-<name>.html |
--no-open |
Do not open the browser automatically |
Click behaviour β click any node to see its info panel. For FUNCTION
and METHOD nodes the panel has a "Show callers" button that switches the
graph into focus mode: only the selected node and every node that calls or
references it are shown, with the caller list in the sidebar. Click empty
space or β Back to return to the full graph.
Uses UNWIND β¦ MERGE Cypher (no APOC required). Every node gets a :Code
label plus a kind-specific label (:Function, :ExternalSymbol, β¦).
Relations are created grouped by type. Install the optional neo4j extra:
pip install "graphlens-cli[neo4j]"| Kind | Description |
|---|---|
PROJECT |
Root project node |
MODULE |
Python/TS/β¦ module (directory or file) |
FILE |
Source file |
CLASS |
Class declaration |
FUNCTION |
Top-level function |
METHOD |
Method inside a class |
PARAMETER |
Function/method parameter |
VARIABLE |
Module-level or local variable |
ATTRIBUTE |
Class attribute |
TYPE_ALIAS |
Type alias declaration |
IMPORT |
Import statement |
DEPENDENCY |
Declared package dependency |
EXTERNAL_SYMBOL |
External symbol (stdlib, third-party, or unknown); carries metadata["origin"] |
BOUNDARY |
Cross-language interface port (HTTP route, queue topic, gRPC method, Temporal activity); shared id collapses matching server/client across languages |
| Kind | Description |
|---|---|
CONTAINS |
Structural containment (project β module β file β class) |
DECLARES |
Declaration (file declares function, class declares method) |
IMPORTS |
Import edge (file β import node) |
RESOLVES_TO |
Import resolved to a module or external symbol |
CALLS |
Function/method call (resolved to declaration node) |
REFERENCES |
Value reference (variable/attribute used as a value) |
INHERITS_FROM |
Class inheritance (resolved to declaration node) |
HAS_TYPE |
Type annotation/inference edge (function/param/variable β class or external) |
DEPENDS_ON |
Package dependency |
EXPOSES |
A server/provider exposes a BOUNDARY (e.g. an HTTP route handler) |
CONSUMES |
A client/consumer consumes a BOUNDARY (e.g. an HTTP call) |
COMMUNICATES_WITH |
Consumer β provider, added by graphlens-link from matching EXPOSES/CONSUMES |
Adapters emit BOUNDARY ports for the interfaces a service exposes or
consumes β HTTP/REST routes and clients, message-queue topics, gRPC
methods, and Temporal activities. Each port has a language-agnostic id
(make_boundary_id(mechanism, key)), so a Python FastAPI route and a
TypeScript fetch call to the same path collapse onto one BOUNDARY
node when their graphs are merged. The graphlens-link package then pairs
CONSUMES with EXPOSES into COMMUNICATES_WITH edges:
from graphlens_link import link_graph
merged = python_graph.merge(ts_graph, allow_shared=True)
result = link_graph(merged) # adds COMMUNICATES_WITH edgesSee examples/demo_cross_language.py for a Python-server β TypeScript-client
walkthrough.
Language adapters register themselves via Python entry points β no changes to the core needed:
# packages/graphlens-python/pyproject.toml
[project.entry-points."graphlens.adapters"]
python = "graphlens_python:PythonAdapter"The registry discovers installed adapters automatically at runtime:
from graphlens import adapter_registry
adapter_registry.available() # ["python", ...]
adapter_cls = adapter_registry.load("python")
adapter = adapter_cls()Adapters can also be registered manually (useful for testing):
adapter_registry.register("python", MyPythonAdapter)Subclass LanguageAdapter and implement four methods:
from pathlib import Path
from graphlens import GraphLens, LanguageAdapter
class MyLangAdapter(LanguageAdapter):
def language(self) -> str:
return "mylang"
def file_extensions(self) -> set[str]:
return {".ml", ".mli"}
def can_handle(self, project_root: Path) -> bool:
return (project_root / "dune-project").exists()
def analyze(
self, project_root: Path, files: list[Path] | None = None
) -> GraphLens:
graph = GraphLens()
files = files or self.collect_files(project_root)
# ... parse and populate graph ...
return graphRegister in pyproject.toml and the core registry finds it automatically.
graphlens/ β uv workspace root (core library)
src/graphlens/ β models, contracts, registry, exceptions, utils
packages/
graphlens-python/ β Python adapter (tree-sitter + ty)
graphlens-typescript/ β TypeScript adapter (tree-sitter + Compiler API)
graphlens-go/ β Go adapter (tree-sitter + gopls)
graphlens-rust/ β Rust adapter (tree-sitter + rust-analyzer)
graphlens-php/ β PHP adapter (tree-sitter + Intelephense)
graphlens-link/ β cross-language linker (COMMUNICATES_WITH)
graphlens-cli/ β CLI (typer): analyze, query, visualize, neo4j
tests/ β core tests (100% coverage)
examples/ β standalone usage examples
Requires Python 3.13+, uv, task.
task install # uv sync --all-groups
task lint # ruff + ty + bandit for all packages
task tests # all tests with coverageIndividual package tasks:
task core:lint task core:test
task python:lint task python:test
task typescript:lint task typescript:test
task cli:lint task cli:testMIT