<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikita Rybalchenko</title>
    <description>The latest articles on DEV Community by Nikita Rybalchenko (@neko1313_4).</description>
    <link>https://dev.to/neko1313_4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995890%2F740d2eb8-e68a-4d9b-9610-9985db2cc588.jpg</url>
      <title>DEV Community: Nikita Rybalchenko</title>
      <link>https://dev.to/neko1313_4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neko1313_4"/>
    <language>en</language>
    <item>
      <title>graphlens: a polyglot code-analysis framework that turns your repo into a typed graph</title>
      <dc:creator>Nikita Rybalchenko</dc:creator>
      <pubDate>Mon, 22 Jun 2026 01:19:45 +0000</pubDate>
      <link>https://dev.to/neko1313_4/graphlens-a-polyglot-code-analysis-framework-that-turns-your-repo-into-a-typed-graph-4mhi</link>
      <guid>https://dev.to/neko1313_4/graphlens-a-polyglot-code-analysis-framework-that-turns-your-repo-into-a-typed-graph-4mhi</guid>
      <description>&lt;h1&gt;
  
  
  graphlens: turn any repo into one typed graph — across Python, TypeScript, Go and Rust
&lt;/h1&gt;

&lt;p&gt;Every code-intelligence tool I've ever used falls into one of two traps.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;grep-and-read loop&lt;/strong&gt;: 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 &lt;code&gt;process_order&lt;/code&gt; you found in &lt;code&gt;services.py&lt;/code&gt; is the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;process_order&lt;/code&gt; that gets called from &lt;code&gt;api.py&lt;/code&gt; — versus the unrelated one in &lt;code&gt;tests/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;single-language silo&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Neko1313/graphlens" rel="noopener noreferrer"&gt;&lt;strong&gt;graphlens&lt;/strong&gt;&lt;/a&gt; is an open-source (MIT) framework built to escape both traps. It parses a source project, normalizes its structure into a shared &lt;strong&gt;graph IR&lt;/strong&gt;, and hands you that graph to do whatever you want with — dependency analysis, navigation, dead-code detection, or feeding an LLM agent precise answers instead of file dumps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository → Language Adapter → GraphLens (IR) → Graph Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language Adapter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parses source files, produces a &lt;code&gt;GraphLens&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GraphLens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Typed nodes + directed relations — the intermediate representation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graph Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Persists or queries the graph (Neo4j, in-memory, your own)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key design decision: &lt;strong&gt;adapters are pure data producers.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  30 seconds to your first graph
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphlens-cli[python]"&lt;/span&gt;
graphlens analyze ./my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphlens&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;adapter_registry&lt;/span&gt;

&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my-project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relations&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nodes_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;called by:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What makes the edges &lt;em&gt;real&lt;/em&gt; (and not name-matching guesses)
&lt;/h2&gt;

&lt;p&gt;Most lightweight code-graph tools resolve references by name: see a call to &lt;code&gt;save()&lt;/code&gt;, draw an edge to anything called &lt;code&gt;save&lt;/code&gt;. That's fast and wrong — there are usually a dozen &lt;code&gt;save&lt;/code&gt;s in a codebase.&lt;/p&gt;

&lt;p&gt;graphlens splits the work in two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tree-sitter&lt;/strong&gt; parses every file into a concrete syntax tree, giving exact structure and 1-based span positions. It records every &lt;em&gt;use-site&lt;/em&gt; as an &lt;strong&gt;occurrence&lt;/strong&gt; with a role (call / read / write / annotation / base).&lt;/li&gt;
&lt;li&gt;A language-specific, &lt;strong&gt;type-aware resolver&lt;/strong&gt; then answers &lt;code&gt;definition_at(file, line, col)&lt;/code&gt; for each occurrence. The resolved definition becomes a real edge to the &lt;em&gt;actual&lt;/em&gt; declaration node.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Resolver&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TyResolver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/astral-sh/ty" rel="noopener noreferrer"&gt;&lt;code&gt;ty&lt;/code&gt;&lt;/a&gt; (Astral, Rust-based) via LSP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TsResolver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the TypeScript Compiler API (Node subprocess)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GoplsResolver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://pkg.go.dev/golang.org/x/tools/gopls" rel="noopener noreferrer"&gt;&lt;code&gt;gopls&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RustAnalyzerResolver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://rust-analyzer.github.io/" rel="noopener noreferrer"&gt;&lt;code&gt;rust-analyzer&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a &lt;code&gt;CALLS&lt;/code&gt; edge points at the real function, a &lt;code&gt;HAS_TYPE&lt;/code&gt; edge at the real class, an &lt;code&gt;INHERITS_FROM&lt;/code&gt; edge at the real base. This is the difference between "probably related" and "is related".&lt;/p&gt;

&lt;h3&gt;
  
  
  Honesty about partial failures
&lt;/h3&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphlens&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RESOLVER_STATUS_KEY&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RESOLVER_STATUS_KEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# 'ok' | 'degraded' | 'unavailable'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI you flip on &lt;code&gt;--strict&lt;/code&gt; and a non-&lt;code&gt;ok&lt;/code&gt; status fails the build, so an agent or dashboard never consumes a graph that's quietly incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nodes&lt;/strong&gt; (&lt;code&gt;PROJECT&lt;/code&gt;, &lt;code&gt;MODULE&lt;/code&gt;, &lt;code&gt;FILE&lt;/code&gt;, &lt;code&gt;CLASS&lt;/code&gt;, &lt;code&gt;METHOD&lt;/code&gt;, &lt;code&gt;FUNCTION&lt;/code&gt;, &lt;code&gt;PARAMETER&lt;/code&gt;, &lt;code&gt;VARIABLE&lt;/code&gt;, &lt;code&gt;ATTRIBUTE&lt;/code&gt;, &lt;code&gt;TYPE_ALIAS&lt;/code&gt;, &lt;code&gt;IMPORT&lt;/code&gt;, &lt;code&gt;DEPENDENCY&lt;/code&gt;, &lt;code&gt;EXTERNAL_SYMBOL&lt;/code&gt;, &lt;code&gt;BOUNDARY&lt;/code&gt;) are frozen dataclasses with an id, kind, qualified name, file path, span, and free-form metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relations&lt;/strong&gt; are directed, typed edges:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CONTAINS&lt;/code&gt; / &lt;code&gt;DECLARES&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;structural containment &amp;amp; declaration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;IMPORTS&lt;/code&gt; / &lt;code&gt;RESOLVES_TO&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;import statements and where they resolve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CALLS&lt;/code&gt; / &lt;code&gt;REFERENCES&lt;/code&gt; / &lt;code&gt;INHERITS_FROM&lt;/code&gt; / &lt;code&gt;HAS_TYPE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;resolved, type-aware edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEPENDS_ON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;declared package dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;EXPOSES&lt;/code&gt; / &lt;code&gt;CONSUMES&lt;/code&gt; / &lt;code&gt;COMMUNICATES_WITH&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;cross-language boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Deterministic IDs
&lt;/h3&gt;

&lt;p&gt;A node's ID is a SHA-256 hash of &lt;code&gt;project::kind::qualified_name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphlens&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_node_id&lt;/span&gt;
&lt;span class="nf"&gt;make_node_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my.module.func&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FUNCTION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → the same id every scan, on every machine
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the ID depends only on identity, not file position, re-scanning yields the same IDs. That's what makes &lt;code&gt;graph.diff(other)&lt;/code&gt; and incremental updates work — and what makes a graph cacheable in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature single-language tools can't have: cross-language boundaries
&lt;/h2&gt;

&lt;p&gt;This is my favorite part. Adapters emit language-agnostic &lt;strong&gt;&lt;code&gt;BOUNDARY&lt;/code&gt;&lt;/strong&gt; nodes for the interfaces a service exposes or consumes — HTTP routes, queue topics, gRPC methods, Temporal activities — with an &lt;code&gt;EXPOSES&lt;/code&gt; edge (provider) or &lt;code&gt;CONSUMES&lt;/code&gt; edge (consumer).&lt;/p&gt;

&lt;p&gt;A boundary's ID is &lt;code&gt;make_boundary_id(mechanism, key)&lt;/code&gt; — &lt;em&gt;no project or language in it&lt;/em&gt;. HTTP paths are normalized so that &lt;code&gt;/users/1&lt;/code&gt;, &lt;code&gt;/users/{user_id}&lt;/code&gt; (FastAPI), &lt;code&gt;&amp;lt;int:id&amp;gt;&lt;/code&gt; (Flask), and &lt;code&gt;:id&lt;/code&gt; (Express) all collapse to &lt;code&gt;GET /users/{}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The payoff: a Python FastAPI route and a TypeScript &lt;code&gt;fetch&lt;/code&gt; to the same endpoint produce the &lt;strong&gt;same&lt;/strong&gt; boundary ID. Merge the two graphs, run &lt;code&gt;graphlens-link&lt;/code&gt;, and you get &lt;code&gt;COMMUNICATES_WITH&lt;/code&gt; edges spanning the language gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphlens&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;adapter_registry&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphlens_link&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;link_graph&lt;/span&gt;

&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)().&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;python_project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typescript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)().&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typescript_project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt;
&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_shared&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# identical BOUNDARY nodes coincide
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;link_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# adds consumer → provider edges
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relations_added&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMMUNICATES_WITH edges added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can answer "which front-end calls hit this endpoint?" — a question no single-language tool can even represent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five ways to use it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;As a library&lt;/strong&gt; — load an adapter, get a &lt;code&gt;GraphLens&lt;/code&gt;, query it: callers, callees, references, neighborhoods, diffs, JSON round-trips, multi-language merges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From the CLI&lt;/strong&gt; — five subcommands cover the common workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphlens analyze ./repo &lt;span class="nt"&gt;--output&lt;/span&gt; graph.json   &lt;span class="c"&gt;# index&lt;/span&gt;
graphlens query process_order &lt;span class="nt"&gt;-g&lt;/span&gt; graph.json &lt;span class="nt"&gt;--op&lt;/span&gt; callers
graphlens visualize ./repo                      &lt;span class="c"&gt;# interactive vis.js HTML&lt;/span&gt;
graphlens neo4j ./repo &lt;span class="nt"&gt;--uri&lt;/span&gt; bolt://localhost:7687
graphlens mcp &lt;span class="nt"&gt;--graph&lt;/span&gt; graph.json                &lt;span class="c"&gt;# serve to agents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In CI&lt;/strong&gt; — &lt;code&gt;--strict&lt;/code&gt; plus a Docker image (&lt;code&gt;ghcr.io/neko1313/graphlens&lt;/code&gt;) with every adapter and toolchain pre-installed. Index on every push, publish the graph as an artifact, fail on a degraded graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To LLM agents over MCP&lt;/strong&gt; — &lt;code&gt;graphlens mcp&lt;/code&gt; exposes a saved graph as Model Context Protocol query tools (&lt;code&gt;stats&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;callers&lt;/code&gt;, &lt;code&gt;callees&lt;/code&gt;, &lt;code&gt;references&lt;/code&gt;, &lt;code&gt;neighbors&lt;/code&gt;, &lt;code&gt;boundaries&lt;/code&gt;, &lt;code&gt;communicates_with&lt;/code&gt;). 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;As a Neo4j export&lt;/strong&gt; — straight into a graph database with &lt;code&gt;UNWIND … MERGE&lt;/code&gt; Cypher (no APOC required), then query it however you like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugin architecture: the SQLAlchemy-dialect pattern
&lt;/h2&gt;

&lt;p&gt;The core never imports an adapter. Each language is a separate package that registers itself via Python entry points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project.entry-points."graphlens.adapters"]&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"graphlens_python:PythonAdapter"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Callers resolve adapters through a registry, by name string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;adapter_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;# ['python', 'typescript', ...]
&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a new language means writing one package against the &lt;code&gt;LanguageAdapter&lt;/code&gt; contract — no changes to the core.&lt;/p&gt;

&lt;h2&gt;
  
  
  What graphlens is &lt;em&gt;not&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The scope is deliberately narrow, and the docs spell it out. graphlens produces a graph IR and stops there. It does &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persist state or own a database (backends are a separate consuming layer);&lt;/li&gt;
&lt;li&gt;watch the filesystem or re-index incrementally on its own (scans are pure functions; deterministic IDs &lt;em&gt;enable&lt;/em&gt; incremental updates, but the caller drives them);&lt;/li&gt;
&lt;li&gt;compute embeddings, semantic search, or relevance ranking (the graph is structural and type-aware, not a vector index);&lt;/li&gt;
&lt;li&gt;provide a UI or an agent runtime (&lt;code&gt;visualize&lt;/code&gt; emits static HTML, &lt;code&gt;mcp&lt;/code&gt; exposes query tools — neither hosts a long-running service).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those belong to tools built &lt;em&gt;on top of&lt;/em&gt; graphlens. Keeping the core minimal is what keeps it composable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;Throughput on real-world projects, refreshed on every release inside the published Docker image (single cold run, indicative):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Lang&lt;/th&gt;
&lt;th&gt;LOC&lt;/th&gt;
&lt;th&gt;Nodes&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Resolved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;apache/superset&lt;/td&gt;
&lt;td&gt;python&lt;/td&gt;
&lt;td&gt;399 519&lt;/td&gt;
&lt;td&gt;156 251&lt;/td&gt;
&lt;td&gt;148.7s&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;colinhacks/zod&lt;/td&gt;
&lt;td&gt;typescript&lt;/td&gt;
&lt;td&gt;74 194&lt;/td&gt;
&lt;td&gt;8 741&lt;/td&gt;
&lt;td&gt;19.0s&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gin-gonic/gin&lt;/td&gt;
&lt;td&gt;go&lt;/td&gt;
&lt;td&gt;23 672&lt;/td&gt;
&lt;td&gt;7 227&lt;/td&gt;
&lt;td&gt;13.9s&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gohugoio/hugo&lt;/td&gt;
&lt;td&gt;go&lt;/td&gt;
&lt;td&gt;224 821&lt;/td&gt;
&lt;td&gt;34 809&lt;/td&gt;
&lt;td&gt;112.7s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BurntSushi/ripgrep&lt;/td&gt;
&lt;td&gt;rust&lt;/td&gt;
&lt;td&gt;50 275&lt;/td&gt;
&lt;td&gt;9 612&lt;/td&gt;
&lt;td&gt;113.1s&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphlens-cli[python]"&lt;/span&gt;
graphlens analyze &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; graph.json
graphlens visualize &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Neko1313/graphlens" rel="noopener noreferrer"&gt;https://github.com/Neko1313/graphlens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://Neko1313.github.io/graphlens/" rel="noopener noreferrer"&gt;https://Neko1313.github.io/graphlens/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requirements:&lt;/strong&gt; Python 3.13+. Python (&lt;code&gt;ty&lt;/code&gt;) and TypeScript (Node) toolchains install on demand; Go and Rust adapters come via the Docker image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>staticanalysis</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
