graphlens: turn any repo into one typed graph — across Python, TypeScript, Go and Rust
Every code-intelligence tool I've ever used falls into one of two traps.
The first is the grep-and-read loop: you (or your AI agent) search for a name, open ten files, read around the matches, follow an import, search again. It works, but it's slow, it burns tokens, and it has no idea that the process_order you found in services.py is the sameprocess_order that gets called from api.py — versus the unrelated one in tests/.
The second is the single-language silo: tools that understand Python beautifully but go blind the moment your TypeScript front end calls a Python FastAPI route. Real systems are polyglot. Your tooling usually isn't.
(Astral, Rust-based) via LSP
TypeScript
TsResolver
the TypeScript Compiler API (Node subprocess)
Go
GoplsResolver
So a CALLS edge points at the real function, a HAS_TYPE edge at the real class, an INHERITS_FROM edge at the real base. This is the difference between "probably related" and "is related".
Honesty about partial failures
Type analysis can degrade — a toolchain is missing, a file doesn't type-check. Instead of silently producing a half-resolved graph, graphlens records the outcome:
In CI you flip on --strict and a non-ok status fails the build, so an agent or dashboard never consumes a graph that's quietly incomplete.
The graph model
Nodes (PROJECT, MODULE, FILE, CLASS, METHOD, FUNCTION, PARAMETER, VARIABLE, ATTRIBUTE, TYPE_ALIAS, IMPORT, DEPENDENCY, EXTERNAL_SYMBOL, BOUNDARY) are frozen dataclasses with an id, kind, qualified name, file path, span, and free-form metadata.
Relations are directed, typed edges:
Kind
Meaning
CONTAINS / DECLARES
structural containment & declaration
IMPORTS / RESOLVES_TO
import statements and where they resolve
CALLS / REFERENCES / INHERITS_FROM / HAS_TYPE
resolved, type-aware edges
DEPENDS_ON
declared package dependency
EXPOSES / CONSUMES / COMMUNICATES_WITH
cross-language boundaries
Deterministic IDs
A node's ID is a SHA-256 hash of project::kind::qualified_name:
CODE
fromgraphlensimportmake_node_id make_node_id("my-project","my.module.func","FUNCTION") # → the same id every scan, on every machine
Because the ID depends only on identity, not file position, re-scanning yields the same IDs. That's what makes graph.diff(other) and incremental updates work — and what makes a graph cacheable in CI.
The feature single-language tools can't have: cross-language boundaries
This is my favorite part. Adapters emit language-agnostic BOUNDARY nodes for the interfaces a service exposes or consumes — HTTP routes, queue topics, gRPC methods, Temporal activities — with an EXPOSES edge (provider) or CONSUMES edge (consumer).
A boundary's ID is make_boundary_id(mechanism, key) — no project or language in it. HTTP paths are normalized so that /users/1, /users/{user_id} (FastAPI), <int:id> (Flask), and :id (Express) all collapse to GET /users/{}.
The payoff: a Python FastAPI route and a TypeScript fetch to the same endpoint produce the same boundary ID. Merge the two graphs, run graphlens-link, and you get COMMUNICATES_WITH edges spanning the language gap:
Now you can answer "which front-end calls hit this endpoint?" — a question no single-language tool can even represent.
Five ways to use it
As a library — load an adapter, get a GraphLens, query it: callers, callees, references, neighborhoods, diffs, JSON round-trips, multi-language merges.
From the CLI — five subcommands cover the common workflows:
In CI — --strict plus a Docker image (ghcr.io/neko1313/graphlens) with every adapter and toolchain pre-installed. Index on every push, publish the graph as an artifact, fail on a degraded graph.
To LLM agents over MCP — graphlens mcp exposes a saved graph as Model Context Protocol query tools (stats, find, callers, callees, references, neighbors, boundaries, communicates_with). Instead of dumping a codebase into the prompt, the agent asks precise questions and gets small structured answers — resolved edges, not best-effort text search.
As a Neo4j export — straight into a graph database with UNWIND … MERGE Cypher (no APOC required), then query it however you like.
Plugin architecture: the SQLAlchemy-dialect pattern
The core never imports an adapter. Each language is a separate package that registers itself via Python entry points:
Adding a new language means writing one package against the LanguageAdapter contract — no changes to the core.
What graphlens is not
The scope is deliberately narrow, and the docs spell it out. graphlens produces a graph IR and stops there. It does not:
persist state or own a database (backends are a separate consuming layer);
watch the filesystem or re-index incrementally on its own (scans are pure functions; deterministic IDs enable incremental updates, but the caller drives them);
compute embeddings, semantic search, or relevance ranking (the graph is structural and type-aware, not a vector index);
provide a UI or an agent runtime (visualize emits static HTML, mcp exposes query tools — neither hosts a long-running service).
Those belong to tools built on top of graphlens. Keeping the core minimal is what keeps it composable.
Benchmarks
Throughput on real-world projects, refreshed on every release inside the published Docker image (single cold run, indicative):
Requirements: Python 3.13+. Python (ty) and TypeScript (Node) toolchains install on demand; Go and Rust adapters come via the Docker image.
If you've ever wanted a single, accurate, language-agnostic model of "how does this codebase actually fit together" — that's exactly what graphlens hands you. I'd love feedback, issues, and adapter contributions.
Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR