<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
 
 <title>Jonathan Popham</title>
 <link href="http://jonathanpopham.github.io/" rel="self"/>
 <link href="http://jonathanpopham.github.io"/>
 <updated>2026-04-24T22:09:01+00:00</updated>
 <id>http://jonathanpopham.github.io</id>
 <author>
   <name>Jonathan Popham</name>
   <email>jonathanpopham+github@gmail.com</email>
 </author>

 
 <entry>
   <title>Supermodel Public API Explainer</title>
   <link href="http://jonathanpopham.github.io/engineering/2026/04/20/supermodel-api-explainer"/>
   <updated>2026-04-20T00:00:00+00:00</updated>
   <id>http://jonathanpopham.github.io/engineering/2026/04/20/supermodel-api-explainer</id>
   <content type="html">&lt;p&gt;The Supermodel API has nine public endpoints. Five of them return graphs. Four of them return things you do with graphs.&lt;/p&gt;

&lt;p&gt;That split is deliberate. The graphs are the primitive. The analyses are applications of the primitive. Useful in their own right, but also a demonstration of what becomes easy once the graph exists. If you don’t see the thing you want on the analysis side, the graph is already there. Build it yourself.&lt;/p&gt;

&lt;p&gt;This post walks through all nine. For each one: what it is, why we ship it, and what it’s good for.&lt;/p&gt;

&lt;p&gt;The general shape of every endpoint is the same. You send a zipped repository. You get back a graph or an analysis. Large jobs return a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;202 Accepted&lt;/code&gt; with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retry-After&lt;/code&gt; and a job handle you can poll; small jobs return the result directly. Authentication is an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Api-Key&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Every request also takes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Idempotency-Key&lt;/code&gt; header: a string you choose that lets us deduplicate identical calls. Post the same key twice and you get the same job back instead of running it again. We recommend a content hash, usually the git commit SHA plus the endpoint name. For the Supermodel graph on next.js that looks like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Idempotency-Key: nextjs:supermodel:a0376cf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you only call one endpoint, make it &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/supermodel&lt;/code&gt;.&lt;/strong&gt; It bundles every primitive below into a single artifact, and it’s what our own internal tools consume by default. The rest of this page is reference for when you want a specific graph on its own. The full spec, including per-endpoint request/response schemas and an interactive playground, lives at &lt;a href=&quot;https://docs.supermodeltools.com&quot;&gt;docs.supermodeltools.com&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-primitives&quot;&gt;The primitives&lt;/h2&gt;

&lt;h3 id=&quot;parse-graph-post-v1graphsparse&quot;&gt;Parse graph: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/parse-graph&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/parse&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; The lowest-level view of your code. We parse every source file with tree-sitter and emit the structural relationships: files contain symbols, symbols declare children, types extend other types, functions reference other functions by name. It’s the AST, flattened into a queryable graph instead of a tree you have to walk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we ship it.&lt;/strong&gt; Every analysis in this API starts here. Parse graphs are what you build on when you want to know not just “what calls what” but “what &lt;em&gt;is&lt;/em&gt; what”: every class, function, type, constant, interface, with its position in the file and its relationship to the symbols around it. If you’re writing your own code intelligence tool, you should not be parsing source files yourself. You should be reading this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Build your own symbol search. Build a custom “find all exports” pass. Layer your own reachability heuristics on top of the declarations we already resolved. Use it as the input to another analysis we haven’t written yet.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;dependency-graph-post-v1graphsdependency&quot;&gt;Dependency graph: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/dependency-graph&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/dependency&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; File-level dependencies. Which file imports which file, across every language in the repo. Follows module resolution conventions per language so the edges actually mean something. The graph distinguishes local dependencies (files inside the repo) from external ones (third-party packages: npm, pip, go modules, crates) by node label: a file that imports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lodash&lt;/code&gt; gets an edge into an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExternalDependency&lt;/code&gt; node; a file that imports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./utils&lt;/code&gt; gets an edge into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LocalDependency&lt;/code&gt; node pointing at another file. One graph, both worlds, one filter to split them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we ship it.&lt;/strong&gt; It’s the coarsest useful view of a codebase. Most architectural questions (“are these two subsystems actually separate?”, “what does this module depend on?”, “is this package a leaf or a hub?”) are questions about the dependency graph. The reason they feel hard to answer with grep is that they’re not string-matching questions. They’re graph questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Enforce layering rules. Find internal files that everyone depends on (the hubs you can’t change cheaply). Find files that depend on everyone (the integration layers). Filter to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExternalDependency&lt;/code&gt; nodes and you have a code-derived SBOM: which of your files actually pulls in which third-party package, not according to your lockfile, but according to the code. Render your architecture diagram from something real instead of something someone drew in 2023.&lt;/p&gt;

&lt;p&gt;On next.js packages/ (2,308 files), the dependency graph comes back with 4,928 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LocalDependency&lt;/code&gt; nodes and 361 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExternalDependency&lt;/code&gt; nodes, connected by 4,422 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;imports&lt;/code&gt; edges. Roughly a 14:1 ratio of internal to external dependencies. A number that tells you something real about the shape of the codebase.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;call-graph-post-v1graphscall&quot;&gt;Call graph: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/call-graph&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/call&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; Function-level calls. Every resolved callsite from one function to another, across files and modules. Not just “file A imports file B”. Actually “function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo&lt;/code&gt; calls function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bar&lt;/code&gt;, on this line, with this resolution.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we ship it.&lt;/strong&gt; The call graph is the thing every AI agent silently wants and doesn’t have. When an agent gets asked to modify a function, the first question it should ask is “who calls this?” The second is “what does this call?” Without a call graph, the agent has to reconstruct the answer with grep, one match at a time, and it will miss the ones that grep can’t see: method dispatch, re-exports, aliased imports. With a call graph, it’s a lookup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Impact analysis. Dead code detection. Refactoring tools that know where the callers are. Pretty much any question that starts with “if I change this function…” is a call-graph query.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;domain-graph-post-v1graphsdomain&quot;&gt;Domain graph: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/domain-graph&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/domain&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; A higher-level grouping of the codebase into &lt;em&gt;domains&lt;/em&gt; and &lt;em&gt;subdomains&lt;/em&gt;, the bounded contexts that would show up on a whiteboard if you asked the team to draw their architecture. The model is loosely based on &lt;a href=&quot;https://c4model.com/&quot;&gt;C4&lt;/a&gt;, which defines four levels: System Context, Container, Component, Code. The four Supermodel graphs line up one-for-one with those levels. The whole codebase graph is the System Context. Domains are Containers, cohesive subsystems that could reasonably live in their own deployable. Subdomains are Components, cohesive groupings within a subsystem. Functions and classes from the parse and call graphs are Code. Same four levels C4 uses, computed from the source instead of drawn in a meeting.&lt;/p&gt;

&lt;p&gt;The computation is a mix of structural and semantic signal. Our graph algorithms produce candidate groupings of nodes that make up the domains and subdomains. An LLM classification pass then names and describes each group, so you get back &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProjectScaffolding&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OptimizationService&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domain_3&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;domain_7&lt;/code&gt;. The output is hierarchical: domains contain subdomains, and subdomains contain the functions, classes, and files that belong to them. IDs line up across every graph in the API, so “show me the call graph restricted to the Auth domain” is a filter, not a separate request.&lt;/p&gt;

&lt;p&gt;Inter-domain edges come back with semantic labels inferred from the code: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coordinates_workflow_with&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;validates_input_for&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transforms_data_for&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;monitors_health_of&lt;/code&gt;, with a generic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMAIN_RELATES&lt;/code&gt; as the fallback. The intent is that the domain graph can be read straight into a diagram or straight into a prompt without a humans-only translation step in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we ship it.&lt;/strong&gt; A call graph with 40,000 nodes isn’t legible. A domain graph with five to ten nodes is. The domain graph is what you hand to a human, or to an agent that’s about to write documentation, or to a reviewer who needs to know which subsystem a PR touches. It’s the zoomed-out picture, computed from the zoomed-in one so the two always agree.&lt;/p&gt;

&lt;p&gt;It also solves the drew-it-once-never-updated problem. Most architecture diagrams live in a slide from 2022. This one regenerates from the code on every request, which means the picture of “what this system is” is always the picture of what it currently is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Auto-generated architecture diagrams that stay honest. PR labels that say which domain changed, useful for routing review to the right team. Onboarding documents that don’t go stale because they’re regenerated from the code. A domain filter on every other graph query, so you can ask “show me the call graph &lt;em&gt;for Auth&lt;/em&gt;” without wading through the rest.&lt;/p&gt;

&lt;p&gt;Run it on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;packages/&lt;/code&gt; tree of next.js (2,308 files) and you get five domains (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NextRuntime&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProjectScaffolding&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OptimizationService&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QualityControl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeveloperTools&lt;/code&gt;) split across 11 subdomains, each with a description, a responsibility list, and the three or four files most central to it. Same input as the other graphs, legible architecture diagram out the other side.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;supermodel-graph-post-v1graphssupermodel&quot;&gt;Supermodel graph: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/supermodel-graph&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/supermodel&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; All of the above, bundled. The Supermodel Intermediate Representation (SIR) is a single artifact that contains the parse graph, dependency graph, call graph, and domain graph, cross-referenced and consistent, in one download. This is the endpoint to reach for by default. If you’re not sure which graph you need, you need this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we ship it.&lt;/strong&gt; Most real tools want more than one of these at once. A dead code detector needs parse + call + entry points. An architecture doc generator needs domain + dependency. Fetching them separately means you pay for four analyses, you stitch them together yourself, and you hope the node IDs line up. The SIR is the version that’s already stitched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Build the tool you actually wanted to build. The SIR is what our own internal analyses consume. If you’re doing anything non-trivial, start here.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-applications&quot;&gt;The applications&lt;/h2&gt;

&lt;p&gt;These are four analyses we ship because we wanted them ourselves, and because each one is a worked example of what the graph is for. You can reproduce any of them from the graph primitives above. We ship them as endpoints because the common cases deserve a one-call answer.&lt;/p&gt;

&lt;h3 id=&quot;dead-code-analysis-post-v1analysisdead-code&quot;&gt;Dead code analysis: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/dead-code-analysis&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/dead-code&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; A ranked list of candidates for deletion. Symbols that are declared in the parse graph but unreachable in the call graph, starting from framework entry points (pages, controllers, route handlers, test files) and walking outward. Each candidate comes with a probability and a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s an endpoint, not a recipe.&lt;/strong&gt; Naive dead code detection is a bad experience. A call graph that doesn’t know about Next.js pages will tell you every page is unused. A parser that doesn’t know about barrel re-exports will flag every re-exported type. The endpoint is the version with those edge cases handled, so you get a list that mostly isn’t noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Run it in CI. Attach it to a PR bot that says “you added a function; here are three near it that we think nothing calls.” Feed it into an agent that’s about to write documentation, so the agent documents what’s alive.&lt;/p&gt;

&lt;p&gt;We wrote about the benchmark results &lt;a href=&quot;https://jonathanpopham.github.io/blog/dead-code-benchmark/&quot;&gt;here&lt;/a&gt;. The short version: on the repo we measured most carefully, the graph-enabled agent was 30× cheaper in tool calls and 5× better at recall than the same agent with only grep.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;test-coverage-map-post-v1analysistest-coverage-map&quot;&gt;Test coverage map: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/test-coverage-map&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/test-coverage-map&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; For every function in the codebase, whether it’s reachable from a test. Not “is this file imported by a test”. Actually, “does a test ever transitively call this function?” Computed from the call graph, with test files as roots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s an endpoint, not a recipe.&lt;/strong&gt; Coverage reports tell you which lines executed. This tells you which functions &lt;em&gt;could&lt;/em&gt; execute from a test entry point. It’s a different question, and it’s more useful when you’re trying to decide what to write tests for, because it’s independent of whether anyone actually ran the suite. A function that’s not reachable from any test has no coverage no matter how high your line-coverage number is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Prioritize where to add tests. Find the functions your critical paths go through that your tests don’t. Pair it with the impact endpoint to find high-blast-radius, low-coverage code: the parts most likely to ship a regression.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;circular-dependency-detection-post-v1analysiscircular-dependencies&quot;&gt;Circular dependency detection: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/circular-dependency-detection&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/circular-dependencies&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; All cycles in the dependency graph, found with Tarjan’s algorithm. Each cycle comes back as an ordered list of files, so you can see exactly which edges to cut to break it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s an endpoint, not a recipe.&lt;/strong&gt; You could run Tarjan’s yourself on the dependency graph. You probably shouldn’t; it’s a five-line function and we already wrote it. More importantly, circular dependencies are the kind of thing that sneaks in while no one is looking, so this is a CI-check endpoint, not a “I wonder if we have any” endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Fail a build when a new cycle appears. Gate merges on cycle count not increasing. When you do find cycles, treat the output as a list of refactoring targets ranked by how much of the codebase they tangle together.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;impact-analysis-post-v1analysisimpact&quot;&gt;Impact analysis: &lt;a href=&quot;https://docs.supermodeltools.com/api-reference/data-plane/impact-analysis&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/impact&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; Blast radius. Given a file or function, the transitive set of callers: everything that could break if you change it. Computed from the reverse call graph, with a depth cap and a grouping by domain so the answer is legible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it’s an endpoint, not a recipe.&lt;/strong&gt; This is the question an agent should be asking before every non-trivial edit, and it’s the question a reviewer should be asking before every approval. “This change touches 3 files” is meaningless. “This change touches 3 files and 127 callers across 4 domains” is the number you actually needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do with it.&lt;/strong&gt; Attach it to your PR bot. Show the blast radius as a comment on every PR. Let your agent call it before it proposes a change so it knows whether it’s editing a leaf function or the thing under half the codebase. When an agent confidently refactors a function with 127 callers because it looked at 3 files, this is the endpoint that would have stopped it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-point-of-the-split&quot;&gt;The point of the split&lt;/h2&gt;

&lt;p&gt;If you squint, the primitives and the applications do the same thing: they take your code and give you back a structured view of it. The difference is where the judgment happens.&lt;/p&gt;

&lt;p&gt;On the primitive side, we make no decisions for you. We give you the graph as it actually exists in the code. What you do with it is your problem, and that’s the feature. If you disagree with our definition of “dead,” our definition of “blast radius,” our definition of “domain,” you can build your own version out of the graph and skip us entirely on that layer.&lt;/p&gt;

&lt;p&gt;On the application side, we make the obvious decisions so you don’t have to. If you want dead code candidates, you want them with framework entry points handled, barrel re-exports handled, generated directories filtered. You don’t want to re-litigate those choices every time. The application endpoints are the version with the defaults that mostly work.&lt;/p&gt;

&lt;p&gt;Both layers are real and both layers are supported. We’d rather ship a good application endpoint and a good primitive for the cases the application gets wrong than ship one without the other.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;against-nextjs&quot;&gt;Against next.js&lt;/h2&gt;

&lt;p&gt;We pointed all nine endpoints at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;packages/&lt;/code&gt; tree of &lt;a href=&quot;https://github.com/vercel/next.js&quot;&gt;vercel/next.js&lt;/a&gt; (commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a0376cf&lt;/code&gt;, 2,308 files, 16MB zipped). Same zip, same API key, one call each.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Endpoint&lt;/th&gt;
      &lt;th&gt;Result&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/parse&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;19,445 nodes / 25,264 edges. 6,927 functions, 2,230 classes, 1,686 types, 361 external packages.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/dependency&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;7,976 nodes / 4,422 imports. 4,928 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LocalDependency&lt;/code&gt; : 361 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExternalDependency&lt;/code&gt; (≈14:1).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/call&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;3,668 functions / 5,943 resolved calls.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/domain&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;5 domains, 11 subdomains. 1,571 files, 6,999 functions, 2,170 classes assigned.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/graphs/supermodel&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;19,463 nodes / 41,791 edges. All of the above, cross-referenced in one artifact.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/dead-code&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;1,876 candidates across 11,248 declarations (~80s).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/test-coverage-map&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;12.8% test-reachable coverage. 877 tested functions, 5,973 untested.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/circular-dependencies&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;9 cycles, 321 files involved, 4 high-severity.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/analysis/impact&lt;/code&gt; (targeted)&lt;/td&gt;
      &lt;td&gt;Top dependents for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;packages/next/src/server/next.ts&lt;/code&gt;: 71. Repo-wide top is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;taskfile.js&lt;/code&gt; at 145.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;One note on impact: calling it without targets or a diff asks for a global coupling map, and on a repo the size of next.js the response blows past the payload limit. That’s the correct behavior. The useful question on a large repo isn’t “give me the entire blast radius of every file,” it’s “what breaks if I change &lt;em&gt;this&lt;/em&gt;?” Scope the call with a diff or a target list and it comes back in about a minute and a half.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;

&lt;p&gt;Every endpoint takes the same input: a zipped repository.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cd /path/to/repo
git archive -o /tmp/repo.zip HEAD

curl -X POST &quot;https://api.supermodeltools.com/v1/graphs/supermodel&quot; \
  -H &quot;X-Api-Key: $SUPERMODEL_API_KEY&quot; \
  -H &quot;Idempotency-Key: $(git rev-parse --short HEAD)&quot; \
  -F &quot;file=@/tmp/repo.zip&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Swap &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/graphs/supermodel&lt;/code&gt; for any of the eight other paths above and the call is identical.&lt;/p&gt;

&lt;p&gt;Full reference lives at &lt;a href=&quot;https://docs.supermodeltools.com&quot;&gt;docs.supermodeltools.com&lt;/a&gt;. The CLI wraps all of this for the live-update workflow:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm install -g @supermodeltools/cli
supermodel watch
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We maintain the graphs. You build the tools.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Why we built Supermodel</title>
   <link href="http://jonathanpopham.github.io/blog/why-we-built-supermodel/"/>
   <updated>2026-04-17T00:00:00+00:00</updated>
   <id>http://jonathanpopham.github.io/blog/why-we-built-supermodel</id>
   <content type="html">&lt;p&gt;When mathematicians get to know each other, one question they may ask is “do you think that math is invented or discovered?”&lt;/p&gt;

&lt;p&gt;This will give the asker an insight into the process of the other mathematician.&lt;/p&gt;

&lt;p&gt;If they say mathematics is invented, they are saying that we begin with a blank slate and make the rules up as we go along.&lt;/p&gt;

&lt;p&gt;If they say that mathematics is discovered, they are saying that everything that can be known is already known, just not by us yet.&lt;/p&gt;

&lt;p&gt;The question is a matter of opinion. Some will give a balanced reply like, “We discover math by inventing mathematical techniques”.&lt;/p&gt;

&lt;p&gt;In software engineering, we could ask a similar question, “do you think that software is invented or discovered?”&lt;/p&gt;

&lt;p&gt;Until recently, the same question was boring to ask software engineers. Every piece of software that was ever written was invented by a person.&lt;/p&gt;

&lt;p&gt;That isn’t true anymore. There’s an alien living in your computer now, and it writes programs that didn’t exist before you asked for them.&lt;/p&gt;

&lt;p&gt;So the question is suddenly interesting. Did you invent your product, or did you discover it?&lt;/p&gt;

&lt;p&gt;A clever engineer might answer the way a clever mathematician does: I discovered my software by inventing new techniques. Fine. But that dodges the thing that actually matters. If our job now involves routinely finding fully-formed worlds on our computers, then our job also now involves understanding what we ship. You can’t be responsible for code you don’t understand. And to understand something, you have to model it.&lt;/p&gt;

&lt;p&gt;That’s what we set out to do. We built Supermodel to make models of agent-written software, so that the humans and agents working on it can actually know what’s there.&lt;/p&gt;

&lt;h2 id=&quot;the-model-has-to-come-from-the-code-not-the-llm&quot;&gt;The model has to come from the code, not the LLM&lt;/h2&gt;

&lt;p&gt;There’s an obvious shortcut: have the LLM write the program, then ask the LLM to explain it. This works on toy problems. It falls apart on real ones.&lt;/p&gt;

&lt;p&gt;The reason is simple. Programs are deterministic. Every symbol means exactly one thing. Every call goes to exactly one place. LLMs are probabilistic. They guess, confidently, based on what similar code usually looks like. A good guess about a real system is still a guess. When the system gets big enough, the guesses compound and the explanation drifts from the code.&lt;/p&gt;

&lt;p&gt;If you want a source of truth about a program, you can’t ask something that hallucinates. You have to read the program itself.&lt;/p&gt;

&lt;p&gt;At the logical level, every program is a graph. Symbols relate to each other in a parse tree. Functions call each other in a call graph. Modules depend on each other in a dependency graph. These aren’t metaphors; they’re the actual structure the compiler sees. A codegraph is all of them together, filtered and labeled so a human or an agent can reason about the system without drowning in it.&lt;/p&gt;

&lt;p&gt;Engineers have always had codegraphs in their heads. The point of pride used to be keeping the whole thing up there. That works when you’re alone. It breaks the moment you need to collaborate, which is why onboarding a new engineer to a mature codebase takes months.&lt;/p&gt;

&lt;p&gt;You are now collaborating with an alien every day. The onboarding problem is no longer monthly. It’s every context window.&lt;/p&gt;

&lt;h2 id=&quot;why-agents-need-this-too&quot;&gt;Why agents need this too&lt;/h2&gt;

&lt;p&gt;You might reasonably ask: if the agent can write code without a graph model, why does it need one to work on code that’s already been written?&lt;/p&gt;

&lt;p&gt;Because writing from scratch and modifying an existing system are different problems. When an agent generates a new function, probabilistic reasoning is an asset. The agent is drawing on everything it’s seen. When an agent edits a real codebase, probabilistic reasoning is a liability. It needs to know, not guess, where a symbol is defined, what calls it, what breaks if it changes. Guessing at the structure of code that already exists is how agents produce confident, plausible, wrong patches.&lt;/p&gt;

&lt;p&gt;An agent grounded in a real codegraph stops guessing about the parts it can look up. We bet that software writers (human or computer) will always need models, and that the correct type of model for software is a graph, and the correct graph model is the one that we have created. We believe that graph models are infrastructure.&lt;/p&gt;

&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;

&lt;p&gt;Our goal is to model any program in any language.&lt;/p&gt;

&lt;p&gt;We’ve distilled our effort into this cli tool:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; @supermodeltools/cli
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Go to your project and run:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;supermodel watch
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’ll get a live graph model of your code that you or your agent can query. More at &lt;a href=&quot;https://supermodeltools.com&quot;&gt;supermodeltools.com&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>What Dead Code Taught Us About Building Tools for AI Agents</title>
   <link href="http://jonathanpopham.github.io/blog/dead-code-benchmark/"/>
   <updated>2026-03-30T00:00:00+00:00</updated>
   <id>http://jonathanpopham.github.io/blog/what-dead-code-taught-us-about-building-tools-for-ai-agents</id>
   <content type="html">&lt;p&gt;We set out to build a code visualization tool. AI can write code faster than you can review it, and we wanted to give developers a way to keep up: interactive architecture graphs, real-time structure views, a shared picture of what’s actually happening in the codebase.&lt;/p&gt;

&lt;p&gt;We quickly realized that to build any type of precise visualization, documentation, or code review, you first need a good graph. The graph is the precursor. And when we looked around, we saw the same thing everywhere: every code review tool, every documentation generator, every AI coding assistant that needs to understand codebase structure ends up building its own parser, its own import resolver, its own symbol graph. It’s the same foundational work rebuilt independently by dozens of teams. Nobody had put together one comprehensive set of graph primitives that’s well-maintained and available for anyone to build on top of.&lt;/p&gt;

&lt;p&gt;So we decided to do that. We think code graphs are a core primitive, especially now, as the industry moves toward software factories where agents need structural understanding of what they’re working on. Our focus is on maintaining precise graphs and parsing so that everyone building on top doesn’t have to duplicate that effort.&lt;/p&gt;

&lt;p&gt;This post is about dead code detection, the first tool we built on our own graph primitives, and the one we’ve benchmarked most extensively. We tested it across 14 real-world repositories, from 449-star libraries to 138K-star monorepos like next.js. The result: &lt;strong&gt;156x cheaper, 11x faster, and 2x better performance than Claude Opus 4.6 alone.&lt;/strong&gt; 94.1% average F1 with 100% precision across every task. But the thesis is bigger than dead code. We aim to make the following case: &lt;strong&gt;graphs are a primitive to code factories.&lt;/strong&gt; This dead code removal tool is an example of what can be built with our public API. If you have your own interpretation of how this problem or another can be better solved with graph primitives, we are happy to provide you with the raw materials to do so.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-dead-code-problem&quot;&gt;The Dead Code Problem&lt;/h2&gt;

&lt;p&gt;We discovered the dead code problem by accident. We gave an agent a directory that had living code and dead code in it and told it to make documentation. It documented dead features as living.&lt;/p&gt;

&lt;p&gt;With vibe-coded software, especially if there are multiple refactors, it’s very likely that there will be dead code left behind clogging the context. In practice, engineers have known when manually coding that it is so frustrating to edit a method and see no change, only to discover that the pattern has drifted from the spec and the method is dead. AI agents don’t have that intuition. They see every function as equally real.&lt;/p&gt;

&lt;p&gt;Dead code clogs context windows, confuses agents, and wastes the most expensive resource in AI-powered development: tokens spent reasoning about code that doesn’t matter.&lt;/p&gt;

&lt;p&gt;We had the insight that good prompting is high signal. We want to provide a high volume of high-signal context to the model and eliminate noise as much as possible. If we could identify and remove dead code before the agent sees it, we could dramatically improve the quality of every downstream task: documentation, code review, refactoring, feature development.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;from-graphs-to-dead-code-candidates&quot;&gt;From Graphs to Dead Code Candidates&lt;/h2&gt;

&lt;p&gt;Our insight was that with a well-made call graph and a well-made dependency graph, in many cases we could discover “dead code candidates.” Naively, if you were to say “anything that is not imported or not called, it is dead.” However, with generated code patterns there may be things that are not called until the system is built. Additionally, framework entry points (Express route handlers, Next.js pages, NestJS controllers) are never “called” by your code; they’re invoked at runtime. Services gated by an API may have code that appears dead but isn’t, since the client could be on the other side of a network boundary: a REST handler with zero internal callers, a webhook endpoint waiting for external events, a plugin loaded by convention rather than by import.&lt;/p&gt;

&lt;p&gt;However, with these constraints in mind, it’s possible to build an agent-enabled system that begins with a set of items that appear to be dead, ranked by probability. An intelligent system could self-improve with certain system knowledge. That is, project structures that follow a generator pattern typically have common directory names like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target/&lt;/code&gt;. So this gives a system that can generate probabilistically more likely dead code candidates, with the caveat that there will be false positives that need to be sorted through.&lt;/p&gt;

&lt;p&gt;Still, this greatly reduces the context load on an LLM. On smaller projects, an LLM can effectively trace the entire execution path inside of the context window. On larger projects this becomes increasingly infeasible. By using graph analysis primitives, we can eliminate a huge chunk of known noise. After that we can use agents to sort through candidates to remove false positives. Finally, over time we can learn how project structures and design patterns create false positives to make a more refined system that further reduces the false positives the agent needs to sort through.&lt;/p&gt;

&lt;p&gt;The cumulative effect of this process is that we can build CI pipelines and refactoring tools that will reduce dead code with increasing accuracy and precision. The final outcome once the dead code is removed is less wasted context, fewer agent errors, and more work done.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;why-naive-reachability-isnt-enough&quot;&gt;Why Naive Reachability Isn’t Enough&lt;/h2&gt;

&lt;p&gt;Static analysis can trace imports and function calls. What it can’t easily see are the boundaries of indirection that make code &lt;em&gt;appear&lt;/em&gt; dead when it isn’t:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework entry points.&lt;/strong&gt; A Next.js &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.tsx&lt;/code&gt;, a NestJS &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Controller()&lt;/code&gt;, an Express route handler. None of these are “called” by your code. They’re invoked by the framework at runtime. A naive dead code detector would flag every API endpoint as unused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-driven and plugin architectures.&lt;/strong&gt; Webhook handlers, message queue consumers, dynamically loaded plugins. All registered through patterns that static analysis struggles to trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API boundaries.&lt;/strong&gt; When a service exposes functions through a REST or GraphQL API, the callers live on the other side of a network boundary. The server-side handler has zero internal callers, but it’s the most critical code in the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated code patterns.&lt;/strong&gt; Code generators (ORMs, gRPC stubs, GraphQL codegen) produce symbols that aren’t called until the rest of the system is wired up. These often live in conventionally-named directories like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;generated/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target/&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__generated__/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-exports and type-level usage.&lt;/strong&gt; A type that’s re-exported through a barrel file (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.ts&lt;/code&gt;), or a constant used only in type annotations. These are alive but invisible to call-graph-only analysis.&lt;/p&gt;

&lt;p&gt;These aren’t edge cases. In a typical production codebase, they represent 30-60% of all exported symbols. Flag them all as dead and you’ve built a tool nobody trusts.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;our-approach-probabilistic-candidates--agent-verification&quot;&gt;Our Approach: Probabilistic Candidates + Agent Verification&lt;/h2&gt;

&lt;p&gt;Instead of trying to build a perfect static analyzer (an impossible task), we designed a system that works &lt;em&gt;with&lt;/em&gt; AI agents rather than replacing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Graph Analysis.&lt;/strong&gt; Parse the codebase with tree-sitter. Build the call graph and dependency graph. Run BFS reachability from identified entry points (framework conventions, main files, test files). Everything unreachable becomes a candidate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Probabilistic Ranking.&lt;/strong&gt; Not all candidates are equally likely to be dead. We rank by signals: Is it in a generated directory? Does it follow a framework naming convention? Is it a type re-export? How deep is it in the import chain? This produces a ranked list of candidates, from “almost certainly dead” to “suspicious but uncertain.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Agent Verification.&lt;/strong&gt; Hand the ranked candidates to an AI agent. The agent can read surrounding code, check for dynamic usage patterns, and apply judgment that static analysis can’t. The key insight: &lt;strong&gt;the agent’s job is now filtering a short list, not searching an entire codebase.&lt;/strong&gt; This is dramatically more tractable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Learn and Refine.&lt;/strong&gt; Track which candidates turn out to be false positives. Learn that projects using Next.js have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.tsx&lt;/code&gt; files that look dead but aren’t. Learn that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__mocks__/&lt;/code&gt; directories are test infrastructure. Feed this back into the ranking model.&lt;/p&gt;

&lt;p&gt;The cumulative effect: each iteration produces fewer false positives for the agent to sort through, the verification gets faster and cheaper, and the system builds institutional knowledge about project patterns.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;benchmarking-how-we-measured&quot;&gt;Benchmarking: How We Measured&lt;/h2&gt;

&lt;p&gt;We used &lt;a href=&quot;https://github.com/greynewell/mcpbr&quot;&gt;mcpbr&lt;/a&gt; (Model Context Protocol Benchmark Runner), built by &lt;a href=&quot;https://github.com/greynewell&quot;&gt;Grey Newell&lt;/a&gt;, to run controlled experiments. Grey also contributed critical fixes to the Supermodel API — including confidence calibration, OOM prevention, dead export detection, and the StreamReader fix that made baseline evaluation reliable — and built the &lt;a href=&quot;https://github.com/supermodeltools/codegraph-bench&quot;&gt;codegraph-bench&lt;/a&gt; code navigation benchmark. The setup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;: Claude Opus 4.6 via the Anthropic API&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Agent harness&lt;/strong&gt;: Claude Code&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Two conditions&lt;/strong&gt;: (A) Agent with Supermodel MCP server providing graph analysis, (B) Baseline agent with only grep, glob, and file reads&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Same prompt, same tools&lt;/strong&gt; (minus the MCP server), same evaluation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;ground-truth-how-do-you-know-whats-actually-dead&quot;&gt;Ground Truth: How Do You Know What’s Actually Dead?&lt;/h3&gt;

&lt;p&gt;This is the hardest part of benchmarking dead code detection. You need to know, with certainty, which symbols in a codebase are dead. We used two approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synthetic codebases.&lt;/strong&gt; We built a 35-file TypeScript Express app and intentionally planted 102 dead code items: legacy integrations, deprecated auth methods, feature flags that were never cleaned up, replaced utility functions. We know exactly what’s dead because we put it there. This is useful for development but doesn’t reflect real-world complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real pull requests from open-source projects.&lt;/strong&gt; This is where the benchmark gets interesting. We searched GitHub for merged PRs whose commit messages and descriptions explicitly mention removing dead code, unused functions, or deprecated features. The logic: if a developer identified code as dead, removed it in a PR, the tests still pass, and the PR was approved by reviewers and merged, that’s confirmed dead code.&lt;/p&gt;

&lt;p&gt;For each PR, we extracted ground truth by parsing the diff: every exported function, class, interface, constant, or type that was &lt;em&gt;deleted&lt;/em&gt; (not moved or renamed) became a ground truth item. The agent’s job is to identify these same items by analyzing the codebase at the commit &lt;em&gt;before&lt;/em&gt; the PR, the state where the dead code still exists.&lt;/p&gt;

&lt;p&gt;This methodology has a key strength: it’s grounded in real engineering decisions, not synthetic judgment calls. A human developer, with full context of the project, decided this code was dead. We’re asking: can an AI agent reach the same conclusion?&lt;/p&gt;

&lt;p&gt;We tested against PRs from 14 repositories spanning small libraries to massive monorepos:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Repository&lt;/th&gt;
      &lt;th&gt;Stars&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;track-your-regions&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;tyr_pr258&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;podman-desktop&lt;/td&gt;
      &lt;td&gt;16K&lt;/td&gt;
      &lt;td&gt;podman_pr16084&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;gemini-cli&lt;/td&gt;
      &lt;td&gt;7K&lt;/td&gt;
      &lt;td&gt;gemini_cli_pr18681&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;jsLPSolver&lt;/td&gt;
      &lt;td&gt;449&lt;/td&gt;
      &lt;td&gt;jslpsolver_pr159&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;strapi&lt;/td&gt;
      &lt;td&gt;71.7K&lt;/td&gt;
      &lt;td&gt;strapi_pr24327&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;mimir&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;mimir_pr3613&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;opentelemetry-js&lt;/td&gt;
      &lt;td&gt;3.3K&lt;/td&gt;
      &lt;td&gt;otel_js_pr5444&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;TanStack/router&lt;/td&gt;
      &lt;td&gt;14K&lt;/td&gt;
      &lt;td&gt;tanstack_router_pr6735&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;latitude-llm&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;latitude_pr2300&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;storybook&lt;/td&gt;
      &lt;td&gt;89.6K&lt;/td&gt;
      &lt;td&gt;storybook_pr34168&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Maskbook&lt;/td&gt;
      &lt;td&gt;1.6K&lt;/td&gt;
      &lt;td&gt;maskbook_pr12361&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;directus&lt;/td&gt;
      &lt;td&gt;34.6K&lt;/td&gt;
      &lt;td&gt;directus_pr26311&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cal.com&lt;/td&gt;
      &lt;td&gt;40.9K&lt;/td&gt;
      &lt;td&gt;calcom_pr26222&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;next.js&lt;/td&gt;
      &lt;td&gt;138K&lt;/td&gt;
      &lt;td&gt;nextjs_pr87149&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Across 60+ benchmark runs, we evaluated both agents on precision (what fraction of reported items are actually dead), recall (what fraction of actually dead items were found), and F1 score.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;results&quot;&gt;Results&lt;/h2&gt;

&lt;h3 id=&quot;the-headline-156x-cheaper-11x-faster-2x-better&quot;&gt;The Headline: 156x Cheaper, 11x Faster, 2x Better&lt;/h3&gt;

&lt;p&gt;Let’s start with the numbers that matter. Across 14 real-world tasks, each drawn from a merged PR in an open-source repository, the graph-enhanced agent using the Supermodel MCP server dominated the baseline agent on every dimension:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;MCP (Graph) Agent&lt;/th&gt;
      &lt;th&gt;Baseline Agent&lt;/th&gt;
      &lt;th&gt;Improvement&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Avg F1&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;94.1%&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;52.0%&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;2x&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Avg Precision&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;varies&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Perfect&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Avg Recall&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;90%&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;varies&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Total Cost&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$1.40&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;$219&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;156x cheaper&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Total Runtime&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;28 min&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;306 min&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;11x faster&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Total Tool Calls&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;28&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;4,079&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;146x fewer&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Avg Tool Calls/Task&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;291&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Head-to-Head Wins&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Ties&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;100% precision across all 14 tasks. Zero false positives. Every single item the graph agent reported was confirmed dead code.&lt;/p&gt;

&lt;p&gt;The baseline agent spent 4,079 tool calls grepping through codebases, trying to reconstruct call graphs at runtime. The graph agent made 28 tool calls total, 2 per task on average. It read the pre-computed analysis, reported the candidates, and was done. &lt;strong&gt;The graph pre-computes the expensive work, so the agent doesn’t have to.&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;per-task-breakdown&quot;&gt;Per-Task Breakdown&lt;/h3&gt;

&lt;p&gt;Here’s every task, sorted by the gap between MCP and baseline performance:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Task&lt;/th&gt;
      &lt;th&gt;Repo&lt;/th&gt;
      &lt;th&gt;Stars&lt;/th&gt;
      &lt;th&gt;MCP F1&lt;/th&gt;
      &lt;th&gt;Base F1&lt;/th&gt;
      &lt;th&gt;MCP P&lt;/th&gt;
      &lt;th&gt;MCP R&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;storybook_pr34168&lt;/td&gt;
      &lt;td&gt;storybook&lt;/td&gt;
      &lt;td&gt;89.6K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;0%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;otel_js_pr5444&lt;/td&gt;
      &lt;td&gt;opentelemetry-js&lt;/td&gt;
      &lt;td&gt;3.3K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;17.6%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;tanstack_router_pr6735&lt;/td&gt;
      &lt;td&gt;TanStack/router&lt;/td&gt;
      &lt;td&gt;14K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;12%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;directus_pr26311&lt;/td&gt;
      &lt;td&gt;directus&lt;/td&gt;
      &lt;td&gt;34.6K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;14.3%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;nextjs_pr87149&lt;/td&gt;
      &lt;td&gt;next.js&lt;/td&gt;
      &lt;td&gt;138K&lt;/td&gt;
      &lt;td&gt;88.9%&lt;/td&gt;
      &lt;td&gt;CRASH&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;80%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;latitude_pr2300&lt;/td&gt;
      &lt;td&gt;latitude-llm&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;92.3%&lt;/td&gt;
      &lt;td&gt;35.3%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;86%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;calcom_pr26222&lt;/td&gt;
      &lt;td&gt;cal.com&lt;/td&gt;
      &lt;td&gt;40.9K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;57.1%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;gemini_cli_pr18681&lt;/td&gt;
      &lt;td&gt;gemini-cli&lt;/td&gt;
      &lt;td&gt;7K&lt;/td&gt;
      &lt;td&gt;80%&lt;/td&gt;
      &lt;td&gt;42.9%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;67%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;podman_pr16084&lt;/td&gt;
      &lt;td&gt;podman-desktop&lt;/td&gt;
      &lt;td&gt;16K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;67.7%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;maskbook_pr12361&lt;/td&gt;
      &lt;td&gt;Maskbook&lt;/td&gt;
      &lt;td&gt;1.6K&lt;/td&gt;
      &lt;td&gt;81%&lt;/td&gt;
      &lt;td&gt;68.4%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;68%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;tyr_pr258&lt;/td&gt;
      &lt;td&gt;track-your-regions&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;97.6%&lt;/td&gt;
      &lt;td&gt;81.6%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;95%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;strapi_pr24327&lt;/td&gt;
      &lt;td&gt;strapi&lt;/td&gt;
      &lt;td&gt;71.7K&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;mimir_pr3613&lt;/td&gt;
      &lt;td&gt;mimir&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;jslpsolver_pr159&lt;/td&gt;
      &lt;td&gt;jsLPSolver&lt;/td&gt;
      &lt;td&gt;449&lt;/td&gt;
      &lt;td&gt;78.3%&lt;/td&gt;
      &lt;td&gt;78.6%&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;64%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The storybook result is striking: 89.6K stars, massive monorepo, and the baseline agent couldn’t find a single confirmed dead code item. The graph agent found all of them. The same pattern plays out across OpenTelemetry JS (17.6% vs 100%), TanStack Router (12% vs 100%), and Directus (14.3% vs 100%). On next.js, the largest repo in the benchmark at 138K stars, the baseline agent crashed entirely. The graph agent scored 88.9% F1.&lt;/p&gt;

&lt;p&gt;The three ties (strapi, mimir, jslpsolver) are instructive. On strapi and mimir, both agents achieved perfect scores – these tasks had clean, well-scoped dead code that even grep-based search could find. On jslpsolver, the baseline agent actually edged out the graph agent by 0.3 percentage points on F1, the only task where that happened. The graph agent’s 100% precision (vs the baseline’s lower precision) shows the tradeoff: the graph agent is more conservative, sometimes missing items the baseline stumbles onto, but never reports false positives.&lt;/p&gt;

&lt;h3 id=&quot;what-changed-from-10-f1-to-94-f1&quot;&gt;What Changed: From 10% F1 to 94% F1&lt;/h3&gt;

&lt;p&gt;If you’ve been following our benchmarking journey, you’ll notice these numbers look dramatically different from our earlier results. In our February and early March runs, the graph agent achieved high recall but terrible precision – single-digit percentages, with hundreds or thousands of false positives per task. What happened?&lt;/p&gt;

&lt;p&gt;Three things changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Parser improvements.&lt;/strong&gt; Barrel re-export filtering, cross-package import resolution, class rescue patterns, and seven new pipeline phases dramatically reduced the candidate list. Fewer false candidates means fewer false positives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. MCP server instead of analysis dump.&lt;/strong&gt; Previously, we pre-computed a large JSON analysis file and handed it to the agent. Files with 6,000+ candidates exceeded tool output limits, causing truncation and errors. The MCP server delivers candidates through a structured API call, solving the file size wall entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Better agent prompting.&lt;/strong&gt; We stopped asking the agent to verify candidates with grep (which was less accurate than the graph analysis it was checking) and instead told the agent to trust the graph analysis. This restored recall to expected levels and, combined with the improved parser, achieved the precision breakthrough.&lt;/p&gt;

&lt;p&gt;The cumulative effect: the same architectural approach – graph-based candidate generation plus agent verification – went from promising-but-rough to production-grade. The thesis was right. The implementation needed iteration.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;failure-modes-we-discovered-and-fixed&quot;&gt;Failure Modes We Discovered (and Fixed)&lt;/h2&gt;

&lt;h3 id=&quot;1-the-file-size-wall-fixed&quot;&gt;1. The File Size Wall (Fixed)&lt;/h3&gt;

&lt;p&gt;Large analysis files exceed tool output limits. A 6,000-candidate analysis exceeds the 25K token tool output limit, so the agent either gets a truncated view or errors out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Moving to the MCP server architecture eliminated this entirely. Instead of dumping a massive JSON file, the agent makes a structured API call and gets back a clean candidate list. This was one of the key changes that took us from single-digit precision to 100%.&lt;/p&gt;

&lt;h3 id=&quot;2-api-recall-gaps-mostly-fixed&quot;&gt;2. API Recall Gaps (Mostly Fixed)&lt;/h3&gt;

&lt;p&gt;Sometimes the Supermodel parser misses ground truth items entirely. In earlier benchmarks, the Logto task found 0 of 8 ground truth items in the analysis. No amount of agent intelligence can find what the analysis doesn’t contain.&lt;/p&gt;

&lt;p&gt;Root causes we identified and fixed: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export default&lt;/code&gt; not tracked, type re-exports (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export type { X } from&lt;/code&gt;) missed, test file imports not scanned, barrel re-export filtering. These parser improvements, combined with the MCP server delivery, are why recall went from 85% to 90% average across a larger and harder set of tasks.&lt;/p&gt;

&lt;h3 id=&quot;3-agent-verification-can-hurt-performance-fixed&quot;&gt;3. Agent Verification Can Hurt Performance (Fixed)&lt;/h3&gt;

&lt;p&gt;This one surprised us. In an earlier benchmark run, we instructed the agent to verify each candidate by grepping for the symbol name across the codebase. The idea was sound: if a symbol appears in other files, it’s probably alive.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;recall dropped from 95.5% to 40%&lt;/strong&gt; on our best-performing task (tyr_pr258). The agent’s grep verification was killing real dead code.&lt;/p&gt;

&lt;p&gt;Why? The grep used word-boundary matching (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep -w&lt;/code&gt;). A function named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hasRole&lt;/code&gt; would match the word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hasRole&lt;/code&gt; appearing in a comment, a string literal, or a completely unrelated variable name in another file. The agent would see the match and mark the function as “alive.” A false negative introduced by the verification step.&lt;/p&gt;

&lt;p&gt;The irony: the static analyzer had already performed proper call graph and dependency analysis to identify these candidates. The agent’s grep check was a &lt;em&gt;less accurate&lt;/em&gt; version of what the analyzer already did. By asking the agent to verify the analysis, we made it worse.&lt;/p&gt;

&lt;p&gt;The fix was simple: tell the agent to trust the analysis and pass through all candidates without grep verification. The lesson: &lt;strong&gt;don’t let a less precise tool override a more precise one.&lt;/strong&gt; Graph-based reachability analysis is strictly more accurate than grep-based name matching for determining whether code is alive.&lt;/p&gt;

&lt;h3 id=&quot;4-agent-non-determinism-mitigated&quot;&gt;4. Agent Non-Determinism (Mitigated)&lt;/h3&gt;

&lt;p&gt;Same task, same config, different results. One run finds 3 true positives; the rerun finds 0. This is an inherent property of LLM-based agents.&lt;/p&gt;

&lt;p&gt;The mitigation that worked: reduce the agent’s degrees of freedom. With the MCP server delivering a short, well-ranked candidate list via a structured API, the agent has almost no room to go off-track. The result is 2 tool calls per task on average, and highly reproducible outcomes. When the agent’s job is “read this list and report it,” non-determinism effectively disappears.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-scaling-insight&quot;&gt;The Scaling Insight&lt;/h2&gt;

&lt;p&gt;This is the finding we keep coming back to. Across the 14 tasks:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Total MCP cost: $1.40.&lt;/strong&gt; Total baseline cost: $219. That’s &lt;strong&gt;156x cheaper&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Total MCP runtime: 28 minutes.&lt;/strong&gt; Total baseline runtime: 306 minutes. That’s &lt;strong&gt;11x faster&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MCP tool calls: 28 total&lt;/strong&gt; (2 per task average). Baseline tool calls: 4,079 total (291 per task average).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The economics are striking. At 2 tool calls per task, the graph agent’s cost is nearly constant regardless of codebase size. The baseline agent’s cost scales with the size and complexity of the repo – 291 tool calls on average, but the variance is enormous. On next.js (138K stars), the baseline agent crashed before producing a result, consuming tokens the entire way.&lt;/p&gt;

&lt;p&gt;As codebases grow:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Baseline cost explodes.&lt;/strong&gt; More files means more tool calls spent building a mental model of the codebase.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Graph cost stays flat.&lt;/strong&gt; The agent makes an MCP call, gets structured candidates, and reports them. Two tool calls whether the repo has 50 files or 50,000.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Baseline quality degrades.&lt;/strong&gt; On the four largest repos (storybook, next.js, strapi, directus – all 34K+ stars), the baseline averaged 28.4% F1. The graph agent averaged 97.2% F1.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Graph quality stays high.&lt;/strong&gt; 90% average recall and 100% precision regardless of repo size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph absorbs the complexity that would otherwise land on the agent. This is the fundamental value proposition, and it applies to any tool built on graph primitives, not just dead code detection.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;lessons-for-building-ai-powered-code-tools&quot;&gt;Lessons for Building AI-Powered Code Tools&lt;/h2&gt;

&lt;h3 id=&quot;1-context-engineering-matters-more-than-model-capability&quot;&gt;1. Context engineering matters more than model capability&lt;/h3&gt;

&lt;p&gt;Same model, same tools, different input structure: 2x better F1, 156x cheaper. The model wasn’t the bottleneck. The signal-to-noise ratio of its input was.&lt;/p&gt;

&lt;p&gt;This is the core lesson. &lt;strong&gt;Good prompting is high-signal prompting.&lt;/strong&gt; The best thing you can do for an AI agent isn’t give it a smarter model. It’s give it pre-computed, structured, relevant context and eliminate the noise.&lt;/p&gt;

&lt;h3 id=&quot;2-pre-compute-what-you-can-delegate-judgment-to-the-agent&quot;&gt;2. Pre-compute what you can, delegate judgment to the agent&lt;/h3&gt;

&lt;p&gt;Static analysis is good at exhaustive enumeration. AI agents are good at judgment calls. The worst outcome is making the agent do both: enumerate &lt;em&gt;and&lt;/em&gt; judge. That’s 291 tool calls per task and $219 total for worse results.&lt;/p&gt;

&lt;p&gt;The best outcome is a pipeline: graphs enumerate candidates, agents verify them. Each component does what it’s best at. Two tool calls per task and $1.40 total for better results.&lt;/p&gt;

&lt;h3 id=&quot;3-precision-is-achievable-not-just-aspirational&quot;&gt;3. Precision is achievable, not just aspirational&lt;/h3&gt;

&lt;p&gt;In our earlier benchmarks, we wrote that “precision is the frontier” – we were achieving high recall but single-digit precision on real codebases. We believed precision was solvable through better ranking and filtering. We were right.&lt;/p&gt;

&lt;p&gt;100% precision across 14 tasks. Zero false positives. The combination of parser improvements, MCP server delivery, and better agent prompting solved a problem we’d been publicly struggling with for months. The lesson: iterate on the system, not the model.&lt;/p&gt;

&lt;h3 id=&quot;4-real-world-codebases-are-dramatically-harder-than-synthetic-ones--but-solvable&quot;&gt;4. Real-world codebases are dramatically harder than synthetic ones – but solvable&lt;/h3&gt;

&lt;p&gt;On our synthetic benchmark, we hit 95% F1. On real-world codebases in earlier runs, F1 was in the single digits despite high recall. We were worried this gap might be fundamental.&lt;/p&gt;

&lt;p&gt;It wasn’t. With the right system improvements, real-world F1 rose to 94.1% average. The four largest repos in our benchmark (storybook at 89.6K stars, next.js at 138K, strapi at 71.7K, directus at 34.6K) averaged 97.2% F1. Repo size is no longer the limiting factor.&lt;/p&gt;

&lt;h3 id=&quot;5-the-system-improves-iteratively&quot;&gt;5. The system improves iteratively&lt;/h3&gt;

&lt;p&gt;Every benchmark run taught us something:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;jsLPSolver taught us that well-organized small repos favor grep-based search&lt;/li&gt;
  &lt;li&gt;Maskbook taught us about the file size wall&lt;/li&gt;
  &lt;li&gt;Logto taught us about parser gaps in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export default&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Directus taught us about the analysis-dump failure mode&lt;/li&gt;
  &lt;li&gt;storybook taught us that the MCP server approach scales to massive monorepos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each lesson fed back into the parser, the delivery mechanism, and the agent prompt. The system got 9x better on F1 (from ~10% to 94.1%) not through model improvements, but through better context engineering.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-bigger-picture-graphs-as-factory-primitives&quot;&gt;The Bigger Picture: Graphs as Factory Primitives&lt;/h2&gt;

&lt;p&gt;The industry is moving toward software factories. Automated pipelines where agents write, review, test, and deploy code with increasing autonomy. These factories need infrastructure primitives. The LLM is becoming commodity. What’s not commodity is the structural understanding of what agents are working on.&lt;/p&gt;

&lt;p&gt;Dead code detection is one application. But the underlying primitive, a structured graph of code relationships, enables an entire category of tools:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Impact analysis&lt;/strong&gt;: “If I change this function, what breaks?” (call graph)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Architecture documentation&lt;/strong&gt;: “What are the domains and boundaries in this system?” (domain graph)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dependency auditing&lt;/strong&gt;: “Which packages are actually used?” (dependency graph)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Refactoring assistance&lt;/strong&gt;: “Show me all the callers of this deprecated API” (call graph)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Security surface mapping&lt;/strong&gt;: “What code paths lead from user input to database queries?” (call graph + data flow)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these has the same structure: pre-compute the graph, rank candidates, let agents handle judgment. The graph is the primitive. The applications are built on top.&lt;/p&gt;

&lt;p&gt;Every team building agent-powered workflows, whether it’s code review, documentation generation, CI pipelines, or full factory orchestration, needs this structural awareness. Right now, most of them are building it from scratch. We think there should be one well-maintained set of graph primitives that everyone can build on, rather than dozens of teams independently duplicating the same foundational work.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-benchmarking-journey-what-we-got-wrong-along-the-way&quot;&gt;The Benchmarking Journey: What We Got Wrong Along the Way&lt;/h2&gt;

&lt;p&gt;Building the dead code tool was one thing. Benchmarking it honestly was harder. Here’s what we learned the hard way.&lt;/p&gt;

&lt;h3 id=&quot;the-evolution-in-numbers&quot;&gt;The evolution in numbers&lt;/h3&gt;

&lt;p&gt;Our benchmark results improved dramatically over three months of iteration:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Period&lt;/th&gt;
      &lt;th&gt;Avg F1&lt;/th&gt;
      &lt;th&gt;Avg Precision&lt;/th&gt;
      &lt;th&gt;Avg Recall&lt;/th&gt;
      &lt;th&gt;Tasks&lt;/th&gt;
      &lt;th&gt;Key Change&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Feb 2026&lt;/td&gt;
      &lt;td&gt;~6%&lt;/td&gt;
      &lt;td&gt;~3%&lt;/td&gt;
      &lt;td&gt;~85%&lt;/td&gt;
      &lt;td&gt;10&lt;/td&gt;
      &lt;td&gt;Initial graph analysis dump&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mar 9, 2026&lt;/td&gt;
      &lt;td&gt;~10%&lt;/td&gt;
      &lt;td&gt;~6%&lt;/td&gt;
      &lt;td&gt;~97%&lt;/td&gt;
      &lt;td&gt;10&lt;/td&gt;
      &lt;td&gt;Parser improvements&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mar 30, 2026&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;94.1%&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;90%&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;14&lt;/td&gt;
      &lt;td&gt;MCP server + prompt fixes&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The jump from 10% to 94% F1 didn’t come from a better model. It came from three system-level changes: parser improvements that reduced false candidates, the MCP server that eliminated the file size wall, and prompt changes that stopped the agent from second-guessing the graph analysis.&lt;/p&gt;

&lt;h3 id=&quot;measuring-the-wrong-thing&quot;&gt;Measuring the wrong thing&lt;/h3&gt;

&lt;p&gt;Our initial benchmark prompt told the agent to read the analysis file, then “verify” each candidate by grepping the codebase to see if the symbol appeared in other files. This seemed rigorous. The agent would filter false positives before reporting.&lt;/p&gt;

&lt;p&gt;It backfired. On our best-performing task (tyr_pr258), recall dropped from 95.5% to 40%. The agent’s grep verification was &lt;em&gt;less accurate&lt;/em&gt; than the graph analysis it was checking. A function named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hasRole&lt;/code&gt; would match the word “hasRole” in a comment, a string literal, or an unrelated variable. The agent would incorrectly mark it as alive.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;don’t verify a precise tool with a less precise tool.&lt;/strong&gt; Graph-based reachability is strictly more accurate than text search for determining if code is reachable.&lt;/p&gt;

&lt;h3 id=&quot;two-layers-of-invisible-caching&quot;&gt;Two layers of invisible caching&lt;/h3&gt;

&lt;p&gt;After implementing parser improvements (barrel re-export filtering, 7 new pipeline phases, class rescue patterns), we ran the benchmark expecting dramatic improvement. The numbers were identical to the previous run.&lt;/p&gt;

&lt;p&gt;It took investigation to discover why: the benchmark had two layers of result caching. A local file cache keyed on the zip hash short-circuited the API call entirely. Even when we busted through that, the API’s server-side idempotency cache returned the old parser’s results because the input hadn’t changed (same repo, same commit, same zip).&lt;/p&gt;

&lt;p&gt;We had to clear the local cache AND change the idempotency key to actually measure the improved parser. Without this, we would have published results that showed “no improvement” when the improvements were real but unmeasured.&lt;/p&gt;

&lt;h3 id=&quot;what-honest-benchmarking-looks-like&quot;&gt;What honest benchmarking looks like&lt;/h3&gt;

&lt;p&gt;These mistakes taught us that benchmark infrastructure has as many failure modes as the system being benchmarked. Our checklist now includes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cache invalidation&lt;/strong&gt;: Clear all analysis caches when the parser changes&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Prompt isolation&lt;/strong&gt;: The benchmark prompt must not introduce behaviors (like grep verification) that interact with what we’re measuring&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Agent behavior logging&lt;/strong&gt;: Always inspect the agent’s transcript, not just the final numbers&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A/B discipline&lt;/strong&gt;: Change one variable at a time (parser version, prompt, agent model) or you can’t attribute results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of our benchmark data, including the runs where we got it wrong, is available in our &lt;a href=&quot;https://github.com/supermodeltools/dead-code-benchmark-blog&quot;&gt;benchmark repository&lt;/a&gt;. Transparency about methodology matters more than impressive numbers.&lt;/p&gt;

&lt;h3 id=&quot;scream-tests-validating-beyond-pr-ground-truth&quot;&gt;Scream tests: validating beyond PR ground truth&lt;/h3&gt;

&lt;p&gt;One thing to note about our benchmarks: our ground truth only captures dead code that a human developer explicitly removed in a PR. In a multi-million line project, there could be lots of dead code that a targeted PR missed. In earlier benchmarks with low precision, some of our “false positives” may have been genuinely dead code that the human developer didn’t catch.&lt;/p&gt;

&lt;p&gt;We’ve begun performing “scream test verification”: systematically deleting reported dead code candidates, then running the project build and CI suite to confirm that things are truly dead. If the tests still pass after deletion, the candidate was genuinely dead regardless of whether a human had flagged it. Early scream test results are consistent with our current precision numbers and have revealed dead code that humans missed.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;try-it&quot;&gt;Try It&lt;/h2&gt;

&lt;p&gt;The Supermodel API is available today. Generate a dead code analysis for your codebase:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create a repo archive&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /path/to/repo
git archive &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; /tmp/repo.zip HEAD

&lt;span class=&quot;c&quot;&gt;# Analyze (via the dead code endpoint)&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST &lt;span class=&quot;s2&quot;&gt;&quot;https://api.supermodeltools.com/v1/analysis/dead-code&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;X-Api-Key: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SUPERMODEL_API_KEY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Idempotency-Key: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git rev-parse &lt;span class=&quot;nt&quot;&gt;--short&lt;/span&gt; HEAD&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-F&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;file=@/tmp/repo.zip&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or use the &lt;a href=&quot;https://github.com/supermodeltools/mcp&quot;&gt;Supermodel MCP server&lt;/a&gt; to give your AI agent direct access to graph analysis in real time.&lt;/p&gt;

&lt;p&gt;The graph endpoints (call graph, dependency graph, domain graph, parse graph) are all available through the same API. Our focus is on maintaining precise graphs and parsing so that you don’t have to. If you’re building agent workflows, code review tools, documentation generators, CI pipelines, or factory orchestration, anything that needs structural understanding of a codebase, we want to be the graph layer you build on top of.&lt;/p&gt;

&lt;p&gt;If you have your own interpretation of how this problem or another can be better solved with graph primitives, we are happy to provide you with the raw materials to do so.&lt;/p&gt;

&lt;p&gt;We maintain the graphs. You build the tools.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;methodology-notes&quot;&gt;Methodology Notes&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Benchmark framework&lt;/strong&gt;: &lt;a href=&quot;https://github.com/greynewell/mcpbr&quot;&gt;mcpbr&lt;/a&gt; by &lt;a href=&quot;https://github.com/greynewell&quot;&gt;Grey Newell&lt;/a&gt;, with Claude Code harness&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;: Claude Opus 4.6 (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;claude-opus-4-6-20260330&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Agent harness&lt;/strong&gt;: Claude Code&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Total benchmark runs&lt;/strong&gt;: 60+ (Feb 6 - Mar 30, 2026)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Total cost&lt;/strong&gt;: ~$220 across all runs (dominated by baseline agent runs)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Repositories tested&lt;/strong&gt;: 14 open-source projects (449 - 138K GitHub stars)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ground truth sources&lt;/strong&gt;: Merged PRs with passing CI from real open-source projects&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;All runs logged&lt;/strong&gt; with timestamps, configs, full agent transcripts, and structured metrics&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Analysis engine&lt;/strong&gt;: Supermodel MCP server (tree-sitter-based parsing, BFS reachability analysis)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Supermodel maintains precise code graph primitives so you don’t have to. &lt;a href=&quot;https://docs.supermodeltools.com&quot;&gt;Get started with the API&lt;/a&gt; or &lt;a href=&quot;https://github.com/supermodeltools/mcp&quot;&gt;try the MCP server&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content>
 </entry>
 
 
</feed>