Blast Radius Analysis Tool: Change Impact Before You Edit
A rename looks small until the build fails in a repository nobody opened. That is the gap a blast radius analysis tool closes: instead of discovering impact after the merge, you ask what a change reaches and get back the direct callers, the transitive dependents behind them, the files and repositories those symbols live in, and a confidence value on every edge.
This page covers how that works on a parsed code graph, how to run change impact analysis for code over MCP, and which parts of the answer a static analysis cannot give you.
What blast radius analysis answers
Three questions hide inside "what breaks if I change this", and they cost different amounts to answer.
- Direct callers: the symbols that call the one you are about to change, one hop out on the call graph. A rename or a new optional argument usually needs only this set.
- Transitive dependents: everything that depends on the behavior of those callers in turn. Code two hops away can rely on a returned value while never naming your symbol in its own source.
- Coverage: the tests that exercise the symbol, so you know whether a safety net exists before you change the thing under it.
Direct callers are the cheap check. The transitive walk is the one that catches breakage outside the file you opened. Both read off the same edge set, so one graph answers them together, in one traversal instead of one search per hop.
The traversal is capped so the answer arrives inside a single tool call, and the result carries a wide_blast_radius signal when it spans more than about five files, crosses architectural layers, or includes cross-repo edges. That signal is a reason to confirm scope with a human, not a reason to stop reading.
Direct callers, transitive dependents, and coverage
Each part of the question has a tool, and the tools compose. Run them in this order.
| Question | Tool | What comes back |
|---|---|---|
| Who calls this symbol directly? | relate with kind: "callers" |
Resolved call sites with file paths, line bounds, and per-edge confidence |
| What depends on it in turn? | relate with kind: "blast_radius" |
The transitive dependent set, layered by distance, including cross-repo rows |
| What implements this interface? | relate with kind: "implementers" |
The classes behind an interface, following heritage and import edges |
| Which tests cover it? | list_tests_for |
Test suites and cases whose suite name matches the symbol |
| How large is the whole change? | estimate_scope |
A pre-flight estimate of files, layers, and repositories a task is likely to touch |
| What does this branch break? | diff_impact |
Changed symbols, the union blast radius, affected routes, and the tests to run |
Resolve the symbol first, check direct callers, and pay for the transitive walk when the change touches a signature or behavior. After a multi-file change, one diff_impact call replaces a loop of per-symbol impact queries.
Graph vs grep, find-references, and embeddings
These methods answer different questions, and only one of them answers "what does this edge lead to".
| Method | What it matches | Where it stops |
|---|---|---|
| Grep or regex | Text, including comments, docs, and unrelated symbols that share the name | No notion of which symbol a match refers to, and no reach into another repository |
| IDE find-references | Symbols in the project the editor has open | It is an editor command, not something an agent can call mid-run, and it does not walk transitive dependents |
| Embedding similarity | Code that reads like the query | Similarity is not connectivity; related-looking code can sit nowhere in the call path |
| Parsed code graph | Calls, imports, implementations, and inheritance between resolved symbols | Sees what the source says at the indexed revision, not what happens at runtime |
Grep fails in both directions at once on impact questions. It returns comments and same-name functions you do not care about, and it misses the call routed through an interface, because the call site names the interface method and never contains the implementation's name. We go deeper on the retrieval distinction in code embeddings vs. code graph and on the three questions inside one refactor in what breaks if I change this function.
A worked example: adding a required argument
Say a shared helper called formatCurrency gains a required locale argument. A text search for the name returns every line that contains it: the real call sites, a doc comment, a test name, and a local variable. It cannot see the caller that goes through an interface method named format, because that call site never mentions formatCurrency. It cannot see the service in a second repository that consumes the shared package.
A graph query resolves the same symbol and returns the call sites as edges, including the interface-routed caller, plus the transitive dependents that read the value flowing out of the helper. The counts in that example vary by repository; the categories do not. The argument and the full diagram are in blast radius analysis.
Cross-repository impact
Inside one repository, impact is a local analysis. Production changes are not local. A shared library ships from its own repository, and two services plus a CLI tool consume it from three others. Change a public method's behavior and the impact set extends into every consumer that calls it, whether or not anyone remembered to check those repositories before merging.
Symvanta links repositories inside a project into one graph, and the edges cross repository boundaries where the code does. Cross-repo edges cover HTTP call sites matched to the route definitions they hit, SQL access matched to the ORM model that owns the table, and queue producers matched to consumers on the same channel, alongside package imports and calls joined on package identity. A cross-repo row can name the package that joined the edge, so you can see why two repositories are connected. index_health reports version drift when one repository pins a dependency at a different version than the sibling repository that provides it.
Pass includeCrossRepo: true on relate to include those rows in a caller or impact answer. If a consumer repository was never attached to the project, it is not in the answer, and the tool reports that repository as not indexed instead of returning a quietly partial list. The mechanics of linking repositories, including HTTP, SQL, and queue transport edges, are covered in code dependency mapping.
Branch and diff impact
Reads default to the default branch. Two operations cover the code you are actually reviewing.
refwithop: "use"pins the session's reads to a tracked feature branch, so the graph matches the branch under review rather than the default branch.refwithop: "index_working_tree"overlays uncommitted edits on a synthetic revision, which is the state of a change before anyone commits it.diff_impactcompares two indexed revisions of one repository, base to head, and returns the changed files and symbols, the union blast radius with layers and cross-repo fallout, the affected HTTP endpoints, the test suites worth running, and git co-change reminders.
Two boundaries are worth knowing before you rely on this. A branch must be tracked before ref resolves it, either automatically from an open pull request in the same repository or from the dashboard, and tracked branch limits depend on the plan (pricing). A working-tree overlay covers the files you pass in your session; source reads and semantic search do not reflect it, because it is not a real commit.
Run change impact analysis over MCP
Every answer above is a tool call against the hosted endpoint, not a report you generate ahead of time. A typical sequence for the worked example looks like this.
init({ repository: "acme/storefront" })
find_node({ selectors: [{ symbol: "formatCurrency" }] })
// -> file path, line bounds, signature, and package identity when the definition has one
relate({
kind: "callers",
selectors: [{ nodeId: "src/money/format.ts:formatCurrency" }],
includeCrossRepo: true
})
// -> resolved call sites with file paths, line bounds, and per-edge confidence
relate({ kind: "blast_radius", selectors: [{ nodeId: "src/money/format.ts:formatCurrency" }] })
list_tests_for({ selector: "formatCurrency" })
diff_impact({ repository: "acme/storefront", base: "main", head: "feature/locale" })
The responses are structured rows, so the agent can map line bounds onto edits and group the impact set by repository and confidence instead of summarizing prose. When a selector is ambiguous, find_node returns { resolved: false, candidates } and the caller picks the node by path rather than trusting a guess.
Add the endpoint once and the queries work from any MCP client:
claude mcp add --transport http symvanta https://mcp.symvanta.com/mcp
The first connection opens an OAuth 2.0 (PKCE) sign-in, so there is no key to generate or paste. The same tools are available through Claude Code, Amp, Cursor, Zed, and any other MCP client; the MCP server overview lists what the endpoint exposes, and the integration guides cover client setup.
Where a static impact analysis stops
A parsed graph sees what the source says. It does not see everything the program does at runtime, and a tool that hides that boundary is worse than one that states it.
- Container and registry resolution. A method resolved from a dependency-injection container by string key at boot time is chosen by configuration, not by a call edge.
- Reflection and computed names. A handler invoked by a name assembled at runtime has no static call site to attach an edge to.
- Plugins and directory scans. A module loaded because it sits in a folder is a runtime relationship unless the registration code is visible in the index.
- Callbacks stored as values. A function placed in a map or handed to an event bus may appear as a reference rather than a call, or not at all once the value is read back by key.
- Raw SQL and dynamic queries. The graph links SQL access to the ORM model that owns a table, not every string-built query.
locatewithmode: "config"finds the remaining writers. - Language coverage. Eleven languages parse into symbol nodes today: TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. Files outside that set, or files that fail to parse, are reachable through text search only, which is mention-level information.
- Scope. The graph holds the repositories attached to the project. A consumer repository nobody attached is not in the answer, and the response says so.
- Freshness. Results describe an indexed revision.
freshnesscompares that revision with the remote head, so a push that landed after the last index is not included. Uncommitted edits in a sibling repository are invisible unless you overlay them in your session, and an overlay applies to what you pass it, not to the whole working tree. - Caps. A wide traversal is capped, and
wide_blast_radiusmarks that the result is at least what came back. Treat a capped answer as a floor, not a total.
Merging runtime signal, traces, and logs back into the static graph is how the first four gaps close. Until then, the honest guarantee is the one worth stating: ordinary calls, imports, implementations, and inheritance resolve, and each row tells you how strongly.
Evaluate it on your repository
The trial flow is the same as the query flow. Create an account, connect GitHub, and pick the repositories to index; the first build produces the symbol graph, and webhooks reindex it on every push. Then add the endpoint to your coding agent and ask the impact question about a helper you already know is risky.
Every plan starts with a 7-day trial that takes no credit card, and self-serve plans start at $19 per month (pricing). Start your free trial and run the first blast radius query against your own callers instead of a demo repository.
If the narrower question is who calls a function, finding callers across repositories covers that directly. For the graph at a higher altitude, read code dependency mapping or start from the MCP server overview.