graphlens: a polyglot code-analysis framework that turns your repo into a typed graph A developer released graphlens, an open-source code-analysis framework that parses source projects into a typed graph with resolved references. The tool uses language-specific adapters and resolvers to produce accurate edges for calls, types, and inheritance, avoiding the common pitfalls of grep-and-read loops and single-language silos. 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 same process 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. graphlens https://github.com/Neko1313/graphlens is an open-source MIT framework built to escape both traps. It parses a source project, normalizes its structure into a shared Repository → Language Adapter → GraphLens IR → Graph Backend | Layer | Responsibility | |---|---| Language Adapter | Parses source files, produces a GraphLens | GraphLens | Typed nodes + directed relations — the intermediate representation | Graph Backend | Persists or queries the graph Neo4j, in-memory, your own | The key design decision: adapters are pure data producers. They never write to a database, never touch the filesystem after reading, never run a server. The graph is the only output. That makes the whole pipeline trivially testable, cacheable, and serializable. pip install "graphlens-cli python " graphlens analyze ./my-project graphlens · my-project nodes: 1240 relations: 3981 resolver: ok nodes by kind relations by kind FUNCTION 410 CONTAINS 980 METHOD 265 DECLARES 870 CLASS 98 CALLS 640 MODULE 54 REFERENCES 410 Or from Python: python from pathlib import Path from graphlens import adapter registry adapter = adapter registry.load "python" graph = adapter.analyze Path "./my-project" print len graph.nodes , "nodes,", len graph.relations , "relations" fn = graph.nodes by name "process order" 0 print "called by:", n.name for n in graph.callers fn.id Most lightweight code-graph tools resolve references by name: see a call to save , draw an edge to anything called save . That's fast and wrong — there are usually a dozen save s in a codebase. graphlens splits the work in two: definition at file, line, col for each occurrence. The resolved definition becomes a real edge to the | Language | Resolver | Engine | |---|---|---| | Python | TyResolver | ty | TsResolver GoplsResolver gopls RustAnalyzerResolver rust-analyzer 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". 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: python from graphlens import RESOLVER STATUS KEY graph.metadata RESOLVER STATUS KEY 'ok' | 'degraded' | 'unavailable' 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. 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 | A node's ID is a SHA-256 hash of project::kind::qualified name : python from graphlens import make 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. 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 ,