<?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>Your coding agent is burning tokens grepping your repo. Here's a one-command fix.</title>
      <dc:creator>Nikita Rybalchenko</dc:creator>
      <pubDate>Sun, 28 Jun 2026 21:58:17 +0000</pubDate>
      <link>https://dev.to/neko1313_4/your-coding-agent-is-burning-tokens-grepping-your-repo-heres-a-one-command-fix-5067</link>
      <guid>https://dev.to/neko1313_4/your-coding-agent-is-burning-tokens-grepping-your-repo-heres-a-one-command-fix-5067</guid>
      <description>&lt;p&gt;&lt;em&gt;graphlens-mcp gives Claude Code, Cursor, and compatible clients a typed graph of your code, so they ask "who calls &lt;code&gt;create_order&lt;/code&gt;?" and get one small answer instead of reading half the codebase. Below: how the engine works, what a 936-run benchmark says about when it actually pays off, and the five-minute install.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've been building graphlens in the open. The full story is three posts on Habr — the engine, a benchmark, the product — but this version stands on its own; I've folded in what matters. Links at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop everyone knows
&lt;/h2&gt;

&lt;p&gt;Picture a big project. A few hundred thousand lines, Python on the backend, TypeScript on the front, a legacy corner nobody wants to touch. You point a coding agent at it and ask something ordinary: "how does auth work here?" or "what breaks if I change this method's signature?"&lt;/p&gt;

&lt;p&gt;The agent can't see the whole repo at once. So it does the only thing it can: grep a name, open a file, read it, follow an import, grep again. It reads a dozen files, every one of them lands in the context window, and every one gets re-billed on the next turn.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical overhead. Anthropic's own engineering blog notes that tool definitions and intermediate results can eat "50,000+ tokens before an agent reads a request" — the window fills before the agent has even started on your question.&lt;/p&gt;

&lt;p&gt;A code graph attacks exactly this. Instead of "read the file and eyeball it," the agent asks a precise question — who calls &lt;code&gt;create_order&lt;/code&gt; — and gets back a small structured answer: resolved edges, not a text search and a prayer.&lt;/p&gt;

&lt;p&gt;That's the pitch. The rest of this post is whether it holds up, and what it took to make it usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the engine actually does
&lt;/h2&gt;

&lt;p&gt;The hard part of "code → graph" isn't drawing boxes. It's the edges. Most lightweight tools resolve references by name: they see a call to &lt;code&gt;save()&lt;/code&gt; and draw an edge to everything named &lt;code&gt;save&lt;/code&gt;. Fast, and wrong — a real codebase has a dozen &lt;code&gt;save&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;graphlens, the engine under the MCP server, splits the work in two:&lt;/p&gt;

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

&lt;p&gt;The resolvers are the same machinery your IDE runs: &lt;code&gt;ty&lt;/code&gt; (Astral's Rust type-checker) for Python, the TypeScript Compiler API for TS, &lt;code&gt;gopls&lt;/code&gt; for Go, &lt;code&gt;rust-analyzer&lt;/code&gt; for Rust. So a &lt;code&gt;CALLS&lt;/code&gt; edge points at the actual function, &lt;code&gt;HAS_TYPE&lt;/code&gt; at the actual class, &lt;code&gt;INHERITS_FROM&lt;/code&gt; at the actual base class. It's the difference between "probably related" and "related." The engine knows the &lt;code&gt;process_order&lt;/code&gt; in &lt;code&gt;services.py&lt;/code&gt; is the one called from &lt;code&gt;api.py&lt;/code&gt;, not the namesake in &lt;code&gt;tests/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That handles the first wall — name ambiguity. The second wall is that most code-intel tools are monolingual. They understand Python beautifully and go blind the moment a TypeScript frontend calls a FastAPI route. Real systems are polyglot; the tools around them usually aren't.&lt;/p&gt;

&lt;p&gt;graphlens emits language-neutral &lt;code&gt;BOUNDARY&lt;/code&gt; nodes for the interfaces a service exposes or consumes: HTTP routes, queue topics, gRPC methods. The boundary ID carries no project and no language, and HTTP paths get normalized so &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 the same key. A FastAPI route and a TypeScript &lt;code&gt;fetch&lt;/code&gt; to that endpoint therefore produce the &lt;em&gt;same&lt;/em&gt; boundary ID. Merge the two graphs, link them, and you get edges crossing the language border — which lets the agent answer "which frontend calls hit this endpoint?", a question a single-language tool can't even phrase.&lt;/p&gt;

&lt;p&gt;Two more choices matter for trust. IDs are deterministic: a node's ID is a SHA-256 of &lt;code&gt;project::kind::qualified_name&lt;/code&gt;, so the same scan yields the same IDs on any machine, which is what makes diffing and incremental updates work. And the graph never lies about being incomplete: if a toolchain is missing or a file fails type-checking, the resolver records a status (&lt;code&gt;ok&lt;/code&gt; / &lt;code&gt;degraded&lt;/code&gt; / &lt;code&gt;unavailable&lt;/code&gt;) instead of quietly handing back a half-resolved graph. In CI, anything but &lt;code&gt;ok&lt;/code&gt; fails the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it actually pay off? 936 runs
&lt;/h2&gt;

&lt;p&gt;Here's the part most "my tool is faster" posts skip. I wrote that first piece, claimed agents burn tokens on grep, and put zero numbers behind it. So I built a benchmark to find out, and the answer surprised me.&lt;/p&gt;

&lt;p&gt;The setup is one controlled variable. Same agent (Claude Code), same prompts, same tasks. The only thing that changes is which MCP server feeds the agent context. Four "hands": &lt;code&gt;filesystem&lt;/code&gt; (grep + read), &lt;code&gt;graphlens&lt;/code&gt; (the structural graph), &lt;code&gt;serena&lt;/code&gt; (an LSP), and &lt;code&gt;codegraph&lt;/code&gt; (a competing graph tool). Three models (Haiku, Sonnet, Opus), three seeds, 26 tasks on &lt;code&gt;apache/superset&lt;/code&gt; (~400k lines, Python + TS). That's 936 runs.&lt;/p&gt;

&lt;p&gt;A few things I locked down so the numbers mean something. The built-in Claude Code tools (Read, Grep, Bash) are disabled — otherwise the agent ignores the MCP server and the test measures nothing. Reference answers are hand-verified against a fixed tag, and crucially not generated by any tool under test. &lt;code&gt;temperature=0&lt;/code&gt; doesn't make these models deterministic, so three seeds and I report the median, not the mean. A run that hits the turn ceiling without answering counts as accuracy 0: "the tool didn't finish in budget," not "no data."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The headline finding: the ranking inverts depending on the task.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On simple point lookups — "where is class X defined," "what does it inherit from" — all four tools tie on accuracy. The only difference is price, a spread of roughly 3×, and graphlens sits unremarkable in the middle. If I'd measured only these, I'd have written "the graph isn't worth it, grep is fine." That would have been half the truth.&lt;/p&gt;

&lt;p&gt;On the work that actually matters — blast-radius questions, finding every override, resolving an ambiguous name — the tools diverge hard:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Tool calls&lt;/th&gt;
&lt;th&gt;$/task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;filesystem (grep)&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;12,596&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;$0.424&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;graphlens&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;748&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$0.018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serena (LSP)&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;1,368&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$0.065&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codegraph&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;1,114&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$0.036&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;grep collapses. Lowest accuracy, and it only reaches an answer in 83% of runs — the rest burn through the 50-turn ceiling. The runs that &lt;em&gt;do&lt;/em&gt; finish cost 10–23× more and take 10–18× longer. Text search drowns in noise when the question is "every call to this" or "which of ten identically-named methods."&lt;/p&gt;

&lt;p&gt;The same graphlens that looked dull on point lookups is now the cheapest ($0.018) and fastest option, answering in a single tool call instead of twenty-seven. That's roughly a 94% token cut against grep on these tasks. codegraph is the most accurate (0.93); serena holds its own.&lt;/p&gt;

&lt;p&gt;There's a second twist I didn't predict: the best tool depends on which &lt;em&gt;model&lt;/em&gt; you run. graphlens returns token-heavy results — graph neighborhoods, reference lists. On a cheap model that verbosity is nearly free, so on Haiku graphlens is the cheapest of all four. On Opus, which prices those same tokens far higher, graphlens becomes the most expensive of the structural tools (still cheaper than grep). serena and codegraph return tight, pointed results and stay cheap on any model.&lt;/p&gt;

&lt;p&gt;Which leads to the one takeaway I'd bet money on: a cheap model on a structural tool beats an expensive model on grep. codegraph + Haiku (~$0.023, ~0.99 accuracy) beats filesystem + Opus (~$0.087, 0.93) on every axis at once.&lt;/p&gt;

&lt;p&gt;One of my predictions flat-out failed, and it's worth reporting. I'd planted cross-language tasks (a TS call resolving to a Python handler across the &lt;code&gt;/api/v1/...&lt;/code&gt; boundary) as a stress test, sure the single-language tools would trip. They didn't — every hand, grep included, solved both. The agent steps across the boundary on its own, whatever feeds it context. A benchmark that only confirms what you hoped isn't a benchmark.&lt;/p&gt;

&lt;p&gt;The honest fine print: one repo, one harness, 26 tasks (20 simple, 6 hard). The cost difference is statistically solid; the accuracy gap on hard tasks is a strong signal but not proven at n=6. &lt;code&gt;cost_usd&lt;/code&gt; is an API-equivalent, not your subscription bill. This is a reproducible measurement on one case, not a universal ranking — and the whole harness plus raw data is open if you want to run it on your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap nobody mentions: an engine isn't a product
&lt;/h2&gt;

&lt;p&gt;So the engine works on the tasks where a graph should work. But there's a hole I glossed over in both Habr posts: the engine is not something you can hand to an agent as-is.&lt;/p&gt;

&lt;p&gt;graphlens, by design, stops at producing the graph. It doesn't own a database, doesn't watch the filesystem, doesn't reindex itself, doesn't raise a long-running service. For an engine that's the right call — a small core is trivial to test, cache, and compose. But to actually wire it into an agent, someone has to write the layer on top: graph storage, invalidation (which files to reindex when one changes), a filesystem watcher, an MCP server with tools the agent can call, registration in each client's config format, and a navigation skill so the agent knows how to use any of it.&lt;/p&gt;

&lt;p&gt;That layer is the work everyone ends up redoing by hand. I wrote it once and packaged it as graphlens-mcp: a thin runtime over the engine that owns the storage, the freshness model, and everything the agent sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  One command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;graphlens-mcp        &lt;span class="c"&gt;# or: pipx install graphlens-mcp&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; graphlens-mcp init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; detects the project's languages, runs a toolchain "doctor," indexes the code into a local graph, writes the MCP server into your agents' configs (it knows Claude Code, Cursor, Windsurf, VS Code/Copilot, Codex CLI, and writes idempotently without clobbering your other servers), and installs the navigation skill. The agent starts the server itself from that config — you never run &lt;code&gt;serve&lt;/code&gt; by hand. Restart the agent and ask it something like "what breaks if I change the signature of &lt;code&gt;create_order&lt;/code&gt;?"&lt;/p&gt;

&lt;p&gt;Requirements: Python ≥ 3.13 (inherited from the engine). MIT licensed. Current version 0.1.2, and it's early — more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent gets
&lt;/h2&gt;

&lt;p&gt;Eight tools, each cut to a specific question about code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_symbols&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full-text search over symbol names — the entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_node_info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source snippet + signature + docstring + position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_file_structure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A file's symbol outline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_callees&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What a function calls (outgoing, to &lt;code&gt;max_depth&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_callers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Who calls a function — the core of impact analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_neighbors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nodes within N hops, any direction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find_references&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non-calls: type annotations, assignments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_cross_language_calls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Links across service boundaries (HTTP/gRPC/queues)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every response carries a graph-quality status (&lt;code&gt;ok&lt;/code&gt; or &lt;code&gt;degraded&lt;/code&gt;), so the agent never mistakes a partial answer for a complete one. Lists are capped and flagged &lt;code&gt;truncated&lt;/code&gt; rather than silently cut.&lt;/p&gt;

&lt;p&gt;The pattern the navigation skill teaches: start at &lt;code&gt;search_symbols&lt;/code&gt;, fan out with &lt;code&gt;get_callers&lt;/code&gt; / &lt;code&gt;find_references&lt;/code&gt;, and only pull &lt;code&gt;get_node_info&lt;/code&gt; for the spots that actually need the source — instead of reading every calling file end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the graph doesn't go stale while you type
&lt;/h2&gt;

&lt;p&gt;What separates a product from "the engine plus a script" is that the graph stays current on its own while you edit.&lt;/p&gt;

&lt;p&gt;A filesystem watcher starts with the server. When a file changes on disk, the server reindexes the &lt;em&gt;connected set&lt;/em&gt; — the changed file, the files that import it, and the files it imports — in one full pass, so cross-file edges rebuild correctly instead of half-breaking. Deleting a file purges its symbols and updates its importers. The case everyone forgets — edits made while the server was off — is handled by a one-shot reconcile on startup: scan the project, index what's new, drop what vanished, refresh what changed, then hand control to the watcher.&lt;/p&gt;

&lt;p&gt;The graph lives in &lt;code&gt;.graphlens/graph.db&lt;/code&gt; (SQLite). It's a regenerable cache, safe to delete; &lt;code&gt;reindex&lt;/code&gt; rebuilds it. Add &lt;code&gt;.graphlens/&lt;/code&gt; to your VCS ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately isn't
&lt;/h2&gt;

&lt;p&gt;Status is early, and I'd rather say so than dress it up. The navigation core works; the rest is in progress.&lt;/p&gt;

&lt;p&gt;The watcher reindexes the connected set of a change, not the whole project — a refactor that ripples through many layers of indirection may need a full &lt;code&gt;reindex&lt;/code&gt; for a perfectly accurate graph. Cross-language &lt;code&gt;COMMUNICATES_WITH&lt;/code&gt; edges rebuild on a full reindex and can erode on incremental edits. Languages other than Python need their toolchains present (Python works out of the box; &lt;code&gt;ty&lt;/code&gt; ships with it); without them a language reports as &lt;code&gt;degraded&lt;/code&gt; — structure parsed, calls and types not fully resolved — but &lt;code&gt;init&lt;/code&gt; never blocks on it, and &lt;code&gt;status&lt;/code&gt; tells you exactly what's missing.&lt;/p&gt;

&lt;p&gt;And the one boundary that's structural, not a roadmap item: graphlens-mcp does not do embeddings or semantic "find me something like this" search. The graph is structural and type-aware, not a vector index. If you need "find code conceptually similar to rate limiting, whatever it's called," that's a vector tool's job. This answers the structural questions: who calls this, what it depends on, what breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and tell me where it breaks
&lt;/h2&gt;

&lt;p&gt;Install it if you run Claude Code, Cursor, or a compatible client on a big polyglot project and you're tired of watching the agent grep its way through the repo — especially if you do impact analysis before refactors, which is exactly the mode where the graph earns its keep. Skip it if your project is small (grep is instant on a few dozen files) or you mainly need semantic search.&lt;/p&gt;

&lt;p&gt;Zero barrier to bail: everything's local, nothing leaves your machine, MIT, and it uninstalls in one command (&lt;code&gt;graphlens-mcp remove --purge-db&lt;/code&gt;). Point it at your main project, confirm the MCP server is live in your agent, and compare the tool-call count on the same architectural question with the graph and without.&lt;/p&gt;

&lt;p&gt;What I need most right now is independent runs on codebases that aren't superset. Issues, numbers from your projects, complaints about tool granularity — all welcome in the repo. The more measurements on different code, the closer this gets to an answer you can carry over, instead of "works on superset."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;graphlens-mcp: github.com/Neko1313/graphlens-mcp&lt;/li&gt;
&lt;li&gt;The engine: github.com/Neko1313/graphlens&lt;/li&gt;
&lt;li&gt;The benchmark: github.com/Neko1313/agent-context-bench&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How much does context cost an AI coding agent? grep vs graph vs LSP, measured across 936 runs</title>
      <dc:creator>Nikita Rybalchenko</dc:creator>
      <pubDate>Wed, 24 Jun 2026 23:10:19 +0000</pubDate>
      <link>https://dev.to/neko1313_4/how-much-does-context-cost-an-ai-coding-agent-grep-vs-graph-vs-lsp-measured-across-936-runs-33m8</link>
      <guid>https://dev.to/neko1313_4/how-much-does-context-cost-an-ai-coding-agent-grep-vs-graph-vs-lsp-measured-across-936-runs-33m8</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/neko1313_4/graphlens-a-polyglot-code-analysis-framework-that-turns-your-repo-into-a-typed-graph-4mhi"&gt;last post&lt;/a&gt; I described &lt;strong&gt;graphlens&lt;/strong&gt; — what it does, how it works — and along the way I casually claimed that an agent "burns tokens grepping around a repo." I gave exactly &lt;strong&gt;zero&lt;/strong&gt; numbers to back that up.&lt;/p&gt;

&lt;p&gt;This post fixes that. Here are the measurements, the data, and a reproducible harness. Spoiler: the conclusion is not the one I expected going in, and that's the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I took &lt;strong&gt;one&lt;/strong&gt; agent (Claude Code), changed &lt;strong&gt;exactly one thing&lt;/strong&gt; — which MCP server feeds it code context — and ran it over 26 tasks on &lt;code&gt;apache/superset&lt;/code&gt;. Four "arms": &lt;code&gt;filesystem&lt;/code&gt; (grep + read), &lt;code&gt;graphlens&lt;/code&gt; (structural graph), &lt;code&gt;serena&lt;/code&gt; (LSP), and &lt;code&gt;codegraph&lt;/code&gt;. Three models (haiku / sonnet / opus), three seeds — &lt;strong&gt;936 runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The headline: &lt;strong&gt;the answer flips depending on the kind of task.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On simple "where is X defined / what does X inherit from" lookups, all four tools are &lt;strong&gt;tied on accuracy&lt;/strong&gt;. The only difference is cost (~3×). graphlens is unremarkable here.&lt;/li&gt;
&lt;li&gt;On "estimate the blast radius / find every override / disambiguate an overloaded name" tasks, the tools &lt;strong&gt;separate hard&lt;/strong&gt;: grep collapses (0.71 accuracy, only 83% of runs even finish, and the ones that do cost &lt;strong&gt;6–24× more&lt;/strong&gt;), while the structural tools stay cheap and accurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I'd only measured the easy tasks, I'd have written "you don't need a graph, grep is fine." If only the hard ones, "you don't need grep, get a graph." The truth sits in the middle, and it's about &lt;strong&gt;what work you hand the agent.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The business case we're actually measuring
&lt;/h2&gt;

&lt;p&gt;Picture a familiar situation. You have a large project: hundreds of thousands of lines, a Python backend, a TypeScript front end, legacy code you're scared to touch. You wire an AI agent into it — for review, refactoring, answering questions like "what breaks if I change this method's signature?"&lt;/p&gt;

&lt;p&gt;The agent can't see the whole repo at once. Something has to feed it context: which functions live where, who calls whom, what inherits from what. And here's an &lt;strong&gt;architectural decision with a price tag&lt;/strong&gt;: what exactly do you feed it?&lt;/p&gt;

&lt;p&gt;There are basically four classes of answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hand it grep + read&lt;/strong&gt; — let it search by text and open files. Zero infrastructure, works everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a structural code graph&lt;/strong&gt; (graphlens) — entity nodes, typed edges, exact answers to "who calls this."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stand up an LSP&lt;/strong&gt; (serena over a language server) — what your IDE already runs on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use an off-the-shelf code-graph product&lt;/strong&gt; (codegraph).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each option costs money (tokens), time (latency), and risk (the agent gives up and hits a turn cap). &lt;code&gt;apache/superset&lt;/code&gt; is an almost perfect stand-in for this case: ~400k LOC, Python + TypeScript, an &lt;code&gt;/api/v1/...&lt;/code&gt; boundary between front and back. A big polyglot project — exactly when this question is worth asking.&lt;/p&gt;

&lt;p&gt;So how much does each option cost? Let's measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment design: change one variable
&lt;/h2&gt;

&lt;p&gt;The whole methodology rests on one principle: &lt;strong&gt;fix everything except one thing.&lt;/strong&gt; Model, system prompt, settings, task set — constants. Only the context-providing MCP server changes. Then any difference in the numbers is the contribution of that tool, not a config accident.&lt;/p&gt;

&lt;p&gt;No tool is designated "the baseline to beat." All four are measured on equal footing, and the numbers rank them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The four arms
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;Context provider (MCP server)&lt;/th&gt;
&lt;th&gt;Indexing step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;filesystem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@modelcontextprotocol/server-filesystem&lt;/code&gt; (read_file + grep)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;graphlens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;graphlens graph over MCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;graphlens analyze&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serena&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Serena (LSP)&lt;/td&gt;
&lt;td&gt;LSP workspace warm-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codegraph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a graph-based competitor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;codegraph init&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One detail that matters for fairness: &lt;strong&gt;Claude Code's built-in tools (Read / Grep / Bash, etc.) are disabled.&lt;/strong&gt; If you don't take them away, the agent ignores the MCP server and falls back to its usual path — and you'd be measuring the wrong thing. So the harness runs &lt;code&gt;claude -p&lt;/code&gt; in a clean room: a fresh &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; with only subscription credentials (no hooks, plugins, skills, memory), &lt;code&gt;--strict-mcp-config&lt;/code&gt; (only this arm's server is visible), &lt;code&gt;--disallowedTools&lt;/code&gt; on every built-in (an explicit &lt;em&gt;deny&lt;/em&gt;, because in headless mode an allow-list alone forbids nothing), and &lt;code&gt;--allowedTools mcp__&amp;lt;server&amp;gt;&lt;/code&gt; to auto-approve the one server.&lt;/p&gt;

&lt;h3&gt;
  
  
  The second axis: models
&lt;/h3&gt;

&lt;p&gt;In parallel I varied the model answering the question:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;model id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;haiku&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-haiku-4-5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sonnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why a second axis becomes clear near the end: &lt;strong&gt;the optimal tool depends on which model you picked.&lt;/strong&gt; That's probably the least obvious finding in the whole thing.&lt;/p&gt;

&lt;p&gt;Total: 4 arms × 3 models × 26 tasks × 3 seeds = &lt;strong&gt;936 runs&lt;/strong&gt; (on Claude Code 2.1.187).&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as an honest measurement
&lt;/h2&gt;

&lt;p&gt;Benchmarks are easy to bend toward the conclusion you want. So the rules are fixed up front — without them the numbers aren't trustworthy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gold answers are hand-verified&lt;/strong&gt; against source at tag &lt;code&gt;6.0.0&lt;/code&gt; (every task carries a &lt;code&gt;file:line&lt;/code&gt; reference). Crucially, &lt;strong&gt;gold is not generated by any tool under test&lt;/strong&gt; (not ty, not pyright, not graphlens itself) — otherwise the comparison is biased toward whoever's output you labelled with. Set-task gold is checked with an independent oracle: Python's &lt;code&gt;ast&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "naive" arm has hands.&lt;/strong&gt; &lt;code&gt;filesystem&lt;/code&gt; is grep + read, not "an agent with no tools." Naive ≠ toolless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index cost is measured separately, once.&lt;/strong&gt; grep pays nothing to index; a graph amortizes. You can't mix those currencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no determinism.&lt;/strong&gt; &lt;code&gt;temperature=0&lt;/code&gt; does not make these models deterministic. So 3 seeds, and the report shows &lt;strong&gt;the median, not the mean.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versions are recorded&lt;/strong&gt; — models and every MCP server — plus a price snapshot and date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cost_usd&lt;/code&gt; is an API-equivalent, not your bill.&lt;/strong&gt; The subscription is flat-rate, so &lt;code&gt;cost_usd&lt;/code&gt; (emitted by the CLI) is what the same tokens &lt;em&gt;would&lt;/em&gt; cost via the API. It's &lt;strong&gt;not&lt;/strong&gt; your actual invoice, but it is a &lt;strong&gt;correct relative $/task metric&lt;/strong&gt; for comparing arms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool, or it doesn't count.&lt;/strong&gt; The system prompt forbids answering from memory; a run with zero tool calls is retried (and a stubborn refusal is tagged &lt;code&gt;__NO_TOOLS__&lt;/code&gt;). Answering "from memory" about a well-known repo wouldn't measure the context provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And separately: &lt;strong&gt;failure counts as accuracy 0.&lt;/strong&gt; If grep hits the 50-turn cap and never produces an answer, that's not "no data" — it's "the tool didn't get there within budget." That's how it's scored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tasks: two regimes, and why you can't blend them
&lt;/h2&gt;

&lt;p&gt;26 tasks split into two classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SIMPLE — 20 pinpoint lookups&lt;/strong&gt; ("where is X defined / what does X inherit from"). One-point answers, checked by substring:&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;#&lt;/th&gt;
&lt;th&gt;What it probes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;where_defined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Python class → defining file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inherits_from&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Python class → base class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abstract_methods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ABC → its abstract methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts_where_defined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;TS hook → defining file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ts_route_call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/v1/...&lt;/code&gt; route → the TS hook that calls it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xlang_link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;TS consumer → Python handler across the API boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;HARD — 6 blast-radius and disambiguation tasks.&lt;/strong&gt; This is the regime where structure and semantics &lt;em&gt;should&lt;/em&gt; beat text search — and which pinpoint lookups simply can't measure:&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;#&lt;/th&gt;
&lt;th&gt;What it probes&lt;/th&gt;
&lt;th&gt;Scoring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disambiguate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;an ambiguous bare method name (e.g. &lt;code&gt;cache_key&lt;/code&gt;, defined on many classes) → &lt;em&gt;the&lt;/em&gt; right class&lt;/td&gt;
&lt;td&gt;substring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;overrides_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;the full set of subclasses overriding a base method&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;set F1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;impact_set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;every file calling a given method (the blast radius)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;set F1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set tasks are scored by F1: reward for recall (find them all), penalty for precision (text search loves to dump every occurrence of &lt;code&gt;.get_indexes(&lt;/code&gt;). Gold sets are kept small (3–5 elements, one ≈17) so they can be exhaustively checked by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I stratify instead of averaging
&lt;/h3&gt;

&lt;p&gt;The set is &lt;strong&gt;deliberately unbalanced&lt;/strong&gt; — 20 simple vs 6 hard. A single blended average would be entirely dictated by the easy tasks and would &lt;strong&gt;hide&lt;/strong&gt; exactly the difference the hard ones expose. So I report each regime &lt;strong&gt;separately, and never mix them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And no, I deliberately don't "balance to 50/50" by dropping simple tasks. That would throw away data and statistical power, and open the door to cherry-picking. Stratification neutralizes the skew &lt;strong&gt;without discarding data&lt;/strong&gt;. (General principle: if regimes give different answers, it's more honest to show both than to bury the conflict under an average.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SIMPLE — 20 pinpoint lookups
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;accuracy&lt;/th&gt;
&lt;th&gt;complete&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;calls&lt;/th&gt;
&lt;th&gt;$/task&lt;/th&gt;
&lt;th&gt;sec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;filesystem&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;1780&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;$0.063&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;graphlens&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;690&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.038&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serena&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;402&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codegraph&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;372&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$0.022&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Accuracy is a &lt;strong&gt;tie&lt;/strong&gt; (formally: Friedman χ²=0.40, not significant). The tools differ only on cost — a ~3× spread — and the terse ones win. &lt;strong&gt;graphlens is unremarkable here&lt;/strong&gt; — a solid mid-pack.&lt;/p&gt;

&lt;p&gt;This is exactly the story a benchmark that &lt;em&gt;only&lt;/em&gt; measured pinpoint lookups would tell: "structural tools are nice, but grep nearly keeps up, and codegraph gives the cheapest answer." And it would be an &lt;strong&gt;incomplete&lt;/strong&gt; truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  HARD — 6 blast-radius and disambiguation tasks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;accuracy&lt;/th&gt;
&lt;th&gt;complete&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;calls&lt;/th&gt;
&lt;th&gt;$/task&lt;/th&gt;
&lt;th&gt;sec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;filesystem&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;12596&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;$0.424&lt;/td&gt;
&lt;td&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;graphlens&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;748&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$0.018&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serena&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;1368&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$0.065&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codegraph&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;1114&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$0.036&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the tools &lt;strong&gt;separate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;grep collapses.&lt;/strong&gt; Lowest accuracy (0.71), only 83% of runs finish (the rest hit the 50-turn cap), and the ones that finish cost &lt;strong&gt;6–24× more&lt;/strong&gt; ($0.42 vs $0.018–0.065) and take &lt;strong&gt;6–18× longer&lt;/strong&gt; (~165s vs 9–29s). Text search drowns in noise when the question is "every call to this" or "which of a dozen identically-named methods."&lt;/p&gt;

&lt;p&gt;And the key bit: &lt;strong&gt;graphlens — the mid-pack tool on easy tasks — is here the cheapest ($0.018) and fastest (9s).&lt;/strong&gt; Its semantic graph finally pays off: one call instead of twenty-seven. The most &lt;em&gt;accurate&lt;/em&gt; tool is codegraph (0.93). serena is competitive (0.85).&lt;/p&gt;

&lt;p&gt;So the same graphlens that looked unremarkable on pinpoint lookups becomes the most economical the moment the work is real — blast radius, refactoring. The ranking &lt;strong&gt;inverts&lt;/strong&gt; between regimes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fairness note. MCP &lt;em&gt;resources&lt;/em&gt; are disabled for all arms. graphlens was the only server exposing resources, and in an early run the agent wandered into enumerating them and inflated cost ~24% until I denied them. All numbers above are from the clean re-run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where the money goes: the mechanism is round-trips
&lt;/h2&gt;

&lt;p&gt;The cost difference is mostly &lt;strong&gt;how many times the agent calls the tool&lt;/strong&gt;, which follows from how a server slices its primitives.&lt;/p&gt;

&lt;p&gt;On a simple "symbol → file" (&lt;code&gt;where_defined&lt;/code&gt;), one call is enough for everyone. The gap opens on &lt;strong&gt;relationship queries&lt;/strong&gt; — inheritance, route → handler, cross-language links. There &lt;code&gt;graphlens&lt;/code&gt; chains fine-grained primitives (&lt;code&gt;find&lt;/code&gt; → &lt;code&gt;neighbors&lt;/code&gt; → &lt;code&gt;references&lt;/code&gt;), while &lt;code&gt;codegraph&lt;/code&gt; packs "source + call paths in one shot" (&lt;code&gt;explore&lt;/code&gt; / &lt;code&gt;node&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This isn't a difference in &lt;em&gt;what the graph knows&lt;/em&gt; — graphs know roughly the same things. It's a difference in API granularity: fewer round-trips → cheaper and faster. That's why codegraph has the efficiency edge on simple tasks, and why grep bankrupts itself on hard ones — it makes 27 round-trips where the graph needs one or two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model × tool interaction: the ranking drifts with model price
&lt;/h2&gt;

&lt;p&gt;This is the least obvious part. Take median $/task (across both regimes) broken down by model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;haiku&lt;/th&gt;
&lt;th&gt;sonnet&lt;/th&gt;
&lt;th&gt;opus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;filesystem&lt;/td&gt;
&lt;td&gt;$0.053&lt;/td&gt;
&lt;td&gt;$0.080&lt;/td&gt;
&lt;td&gt;$0.087&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;graphlens&lt;/td&gt;
&lt;td&gt;$0.020&lt;/td&gt;
&lt;td&gt;$0.041&lt;/td&gt;
&lt;td&gt;$0.046&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serena&lt;/td&gt;
&lt;td&gt;$0.026&lt;/td&gt;
&lt;td&gt;$0.033&lt;/td&gt;
&lt;td&gt;$0.042&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codegraph&lt;/td&gt;
&lt;td&gt;$0.023&lt;/td&gt;
&lt;td&gt;$0.041&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cheapest-first ranking &lt;strong&gt;within each model&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;haiku:&lt;/strong&gt; graphlens \$0.020 &amp;lt; codegraph \$0.023 &amp;lt; serena \$0.026 &amp;lt; filesystem \$0.053&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sonnet:&lt;/strong&gt; serena \$0.033 &amp;lt; graphlens \$0.041 &amp;lt; codegraph \$0.041 &amp;lt; filesystem \$0.080&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;opus:&lt;/strong&gt; codegraph \$0.031 &amp;lt; serena \$0.042 &amp;lt; graphlens \$0.046 &amp;lt; filesystem \$0.087&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch what happens to graphlens. On &lt;strong&gt;haiku it's the cheapest of all.&lt;/strong&gt; On &lt;strong&gt;opus it becomes the most expensive of the structural tools&lt;/strong&gt; (still cheaper than grep, though).&lt;/p&gt;

&lt;p&gt;The mechanism: graphlens results are &lt;strong&gt;token-heavy&lt;/strong&gt; — graph neighborhoods, reference lists. On a cheap model that verbose context is nearly free; on an expensive one, opus prices the same tokens far higher, and verbosity hits the wallet. &lt;strong&gt;serena and codegraph stay cheap on any model&lt;/strong&gt; because they return pinpoint results — they're robust to model choice; graphlens isn't.&lt;/p&gt;

&lt;p&gt;Which gives the most valuable takeaway of the lot: &lt;strong&gt;a cheap model on a structural tool beats an expensive model on grep.&lt;/strong&gt; codegraph + haiku (~$0.023, accuracy ~0.99) beats filesystem + opus (~$0.087, accuracy 0.93) on every axis at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis that didn't hold
&lt;/h2&gt;

&lt;p&gt;I planted the two &lt;code&gt;xlang_link&lt;/code&gt; tasks as a stress test: a TS call resolves to a Python handler across the &lt;code&gt;/api/v1/...&lt;/code&gt; boundary, and I was sure single-language tools would trip on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They didn't.&lt;/strong&gt; Every arm, grep included, solved both cross-language tasks. The agent steps across the boundary itself, regardless of the context provider. On this set the hypothesis failed, and I report that as loudly as the findings that held. A benchmark that only reports what it hoped to see isn't a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics, honestly
&lt;/h2&gt;

&lt;p&gt;Friedman test across the four tools, over task blocks, within each regime (df=3; critical values: 0.05 → 7.82, 0.01 → 11.34):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SIMPLE:
  accuracy  n=20  χ²= 0.40  (n.s.)    — tie
  cost      n=20  χ²=18.42  (p&amp;lt;.01)   — serena &amp;lt; codegraph &amp;lt; graphlens &amp;lt; filesystem

HARD:
  accuracy  n= 6  χ²= 3.50  (n.s.)    — underpowered
  cost      n= 6  χ²=11.80  (p&amp;lt;.01)   — graphlens &amp;lt; codegraph &amp;lt; serena &amp;lt; filesystem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's honest to claim from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;cost difference is significant in both regimes&lt;/strong&gt; (p&amp;lt;.01). On HARD, graphlens is reliably the cheapest and grep reliably the most expensive. That's a solid result.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;accuracy gap on HARD is large but not statistically significant&lt;/strong&gt; at n=6 (χ²=3.50). It's a strong &lt;em&gt;descriptive&lt;/em&gt; signal, not a proven one. Six tasks is few.&lt;/li&gt;
&lt;li&gt;To firm up the accuracy claim you'd &lt;strong&gt;add hard tasks, not cut simple ones.&lt;/strong&gt; Trimming the simple regime gives the hard one zero extra power — it just throws away good data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm leaving this in the article on purpose. The temptation to write "graphlens/codegraph are more accurate than grep, proven" is real, but n=6 doesn't carry it, and pretending otherwise would be dishonest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index amortization: different currencies
&lt;/h2&gt;

&lt;p&gt;The structural tools build an index once — &lt;strong&gt;pure static work, zero LLM tokens&lt;/strong&gt;, wall-clock only:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;one-time index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;filesystem&lt;/td&gt;
&lt;td&gt;0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codegraph&lt;/td&gt;
&lt;td&gt;48s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;graphlens&lt;/td&gt;
&lt;td&gt;84s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serena&lt;/td&gt;
&lt;td&gt;94s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;grep pays nothing up front but pays more per query. These are &lt;strong&gt;different currencies&lt;/strong&gt; (seconds vs $/tokens), so I draw no single "break-even point" — that'd be a stretch. The picture is simple: the index is a one-time time cost with not a single token spent, while the $/task savings drip on every task. Over a long session the structural tools amortize; on a couple of one-off queries, grep's zero setup can win on time-to-first-answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways for the business case
&lt;/h2&gt;

&lt;p&gt;Back to the original question: what do you feed the agent on a large project?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no "this tool is always best" answer.&lt;/strong&gt; There's a "depends on what work you hand it" answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-off pinpoint lookups&lt;/strong&gt; ("where is this class defined," "what does it inherit from"): use anything. grep keeps up, accuracy is the same, zero setup. You pay at most a small token overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustained blast-radius work&lt;/strong&gt; — refactoring, impact analysis, disambiguation on a large base: structural tools cut cost &lt;strong&gt;6–24×&lt;/strong&gt; and latency &lt;strong&gt;6–18×&lt;/strong&gt; vs grep — and, just as important, &lt;strong&gt;they don't hit the turn cap.&lt;/strong&gt; grep on these tasks isn't just expensive; 17% of the time it never reaches an answer at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model choice interacts with tool choice.&lt;/strong&gt; A verbose graph is cheap on a small model and pricey on a big one. Running opus? Pick a tool with pinpoint output (codegraph, serena). Running haiku? graphlens is suddenly the cheapest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The cheapest combo isn't "expensive model + simple tool" — it's "cheap model + structural tool."&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the honest caveats, without which you can't transfer the conclusions to your project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One repository (&lt;code&gt;apache/superset&lt;/code&gt; @ 6.0.0), one harness, 26 tasks (20 simple / 6 hard). Regimes are reported separately and &lt;strong&gt;never blended&lt;/strong&gt;. &lt;code&gt;cost_usd&lt;/code&gt; is an API-equivalent, not a subscription bill. Failure = accuracy 0. This is &lt;strong&gt;not a universal ranking&lt;/strong&gt; — it's a reproducible measurement on one concrete case.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where graphlens fits
&lt;/h2&gt;

&lt;p&gt;Since this is a follow-up to &lt;a href="https://dev.to/neko1313_4/graphlens-a-polyglot-code-analysis-framework-that-turns-your-repo-into-a-typed-graph-4mhi"&gt;the graphlens post&lt;/a&gt;, let me say it straight. This benchmark does &lt;strong&gt;not&lt;/strong&gt; prove graphlens is "the best." It shows the &lt;strong&gt;specific regime where its structural graph pays off&lt;/strong&gt; (impact analysis, cheap and fast on cheaper models), and just as plainly shows &lt;strong&gt;where it lags&lt;/strong&gt; (on opus its verbose output costs more than codegraph and serena; codegraph is more accurate on hard tasks).&lt;/p&gt;

&lt;p&gt;For me that's more useful than any victory lap. graphlens was built as an &lt;strong&gt;engine and a precise polyglot graph model&lt;/strong&gt;, not a turnkey app — and the benchmark confirms exactly that: on structural questions the graph beats text search by a wide margin, and there's clear room to grow — MCP tool granularity (fewer round-trips, like codegraph) and output compactness (so it doesn't bankrupt itself on expensive models). That's my next work item, now backed by numbers instead of intuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The whole harness and the raw data are open. A run reassembles deterministically from &lt;code&gt;data/&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark repo:&lt;/strong&gt; &lt;a href="https://github.com/Neko1313/agent-context-bench" rel="noopener noreferrer"&gt;https://github.com/Neko1313/agent-context-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;See &lt;code&gt;metrics.ipynb&lt;/code&gt; (all charts and per-section stats) and &lt;code&gt;README.md&lt;/code&gt; (methodology).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uv run main.py&lt;/code&gt; runs the full pipeline (clone superset → build indices → 936 runs, resumable within subscription limits), then open &lt;code&gt;metrics.ipynb&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've got a large project of your own and the itch to run the harness on it — issues and results welcome. The more independent runs across different codebases, the closer we get to an answer that transfers, rather than "works on superset."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(More on the tool itself: &lt;a href="https://github.com/Neko1313/graphlens" rel="noopener noreferrer"&gt;graphlens&lt;/a&gt; · &lt;a href="https://neko1313.github.io/graphlens/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <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>
