Blog

Blast Radius Analysis: What Breaks First

An agent renames a shared helper, runs the tests it can see, and ships a diff that breaks three services it never looked at. Picture the shape of it: a helper with fourteen callers, of which grep finds nine, misses one routed through an interface, and has no way to know about the four living in a different repository. Blast radius, done right, is a call the agent makes mid-session, before it writes the edit, answered in the time it takes for one more tool call.

What blast radius means for an agent

Static-analysis desktop tools built for human architects have scanned dependency graphs overnight for years and produced impact reports for review before a big refactor. That workflow suits a human with a quarter to plan. An agent making dozens of small edits in a single session, the kind of long autonomous run Windsurf Cascade is built for, needs the same answer in milliseconds, as a callable primitive: give it a symbol, get back the direct and transitive dependents, in the time it takes to make one more tool call.

The worked example: renaming a shared helper

Say a shared helper, formatCurrency, needs a signature change: add a required locale parameter. An agent working from grep searches for formatCurrency and gets back every line that contains the string. Some of those are the real call sites. Some are a comment referencing the function by name. One is a call routed through an interface where the concrete implementation is named formatCurrency but the call site only ever references the interface method format, so grep never finds it at all. The agent, working from an incomplete and noisy list, either misses the interface-routed caller and ships a broken build, or plays it safe and touches every string match including the comment, which is its own kind of wrong.

A graph-backed lookup resolves the same question differently. formatCurrency is a node. Every real call to it, including the one routed through the interface, is an edge, because the graph is built by parsing calls and implementations. Ask for the callers and you get the real set: the direct call sites, resolved through the interface where one exists, with no comments and no false positives. Ask for the blast radius and you get one layer further out: what breaks in the callers of those callers if the signature changes underneath them.

The table below is the shape of the difference on this worked example, not a measurement of any particular repository:

Question Grep on formatCurrency Graph relate(kind: callers)
Direct string matches Every line containing the name, comments included n/a, resolves symbols not strings
Interface-routed caller Missed entirely Found, resolved through format
Comment mentioning the name Included as a false positive Excluded
Transitive callers of callers Not answered Returned as the blast radius set
Cross-repo callers Not answered Returned when repos are linked

This is the same shape of gap that shows up on every rename, signature change, or delete once a codebase has more than a handful of files and at least one interface in the mix.

Direct vs transitive dependents

Two different questions get asked here, and agents, along with the tools built for them, frequently collapse them into one. "Who calls this function" is a direct-dependents question: one hop out on the call graph. "What breaks if I change this function's behavior" is transitive: it has to walk beyond the immediate callers into whatever depends on those callers in turn, because a function two hops away can depend on behavior that never appears in its own source. A direct-callers answer is cheap and often sufficient for a simple rename. A real blast radius answer walks the full transitive closure and returns it as a set an agent can reason over.

Cross-repo blast radius

The hardest version of this question crosses a repository boundary. A shared library ships from its own repo; two backend services and a CLI tool consume it from three other repos. Change a public method's behavior in the library and the blast radius extends into every consumer repo that calls that method, whether or not anyone remembered to check those other repos before merging. An agent working repo-by-repo, which is how most agents work by default, has no visibility into this. It needs the library repo and every consumer repo indexed into the same graph, with edges that cross the repository boundary the way real dependencies do. That's a structural requirement: no instruction in a prompt gives an agent visibility into repos it was never shown.

A blast radius diagram: a teal-highlighted formatCurrency node in repo A radiates blue edges to a direct-caller service, a transitive dependent test, and a direct-caller handler, a teal edge to an interface-routed caller resolved via format(), and teal cross-repo edges across a dashed boundary to a backend service and a CLI tool in a linked repo B
A signature change to formatCurrency and its impact surface: direct callers, a transitive dependent, an interface-routed caller resolved through format(), and cross-repo consumers reached across a linked-repo boundary. Link to this diagram Open full size

How agents guess today

Absent a real blast radius answer, agents fall back on a mix of grep and pattern recognition: search for the symbol name, read a few of the matching files, infer the rest from naming conventions and folder structure. It works often enough on small, well-organized codebases to build false confidence, and it fails quietly on the codebases where it matters most: large, inconsistently named, with interfaces and dependency injection in the mix. The agent ships the edit and reports it as done, confident by default because nothing in its process flagged uncertainty.

Blast radius as an MCP call

This is what a graph makes callable. Symvanta exposes a relate tool over MCP with kind: "blast_radius": pass a selector for the symbol you're about to change and get back the callers, the transitive dependents, and, where the repositories are linked, the cross-repo callers too, resolved from parsed calls and implementations. The same relate tool answers narrower questions as well: direct callers, dependencies, interface implementers, so an agent can start with a cheap direct-callers check and only pay for the full blast radius when the change actually warrants it. Selectors accept a nodeId or a plain symbol name, and the response returns file paths and line bounds an agent can hand straight to its editor, whether that's Claude Code, Cursor, or any other MCP client. This is one instance of the broader pattern we cover in why AI coding agents fail on large codebases: a graph turns a connectivity question into a lookup.

Embeddings alone can't answer this kind of question, because similarity search ranks how much two snippets resemble each other, a different signal than which symbols call which; we go into that distinction directly in code embeddings vs code graph. If you're evaluating this space more broadly, see how this compares to Augment Code, another tool agents reach for when navigating unfamiliar code.

Known limits

A parsed call graph sees what's written in the source. It does not reliably see everything that happens at runtime. Dynamic dispatch through a container that resolves an implementation by string key at boot time, reflection that invokes a method by name computed from a config value, plugins loaded from a directory scan, these are real edges in the running system that a static graph can miss or represent only partially. The real fix for that class of problem is runtime signal, traces and logs, merged with the static graph. A blast radius tool worth trusting gets the static case right, ordinary calls and interfaces with no false negatives, and states plainly which dynamic cases it can't fully resolve.

Blast radius is easiest to grasp on a codebase you can see: the Cal.com architecture page shows the kind of shared hub (its HttpError type) where a single change fans out across dozens of call sites.

Want to see blast radius run against your own repository, with your own callers and your own interfaces? Start a free trial: 7 days, no credit card.

Start free trial →