Blog

What Breaks When You Change a Function

Every change to a function starts with the same question: what else moves if I move this? Say getUser needs a required tenantId argument. The edit itself takes ten seconds. The risk lives entirely in the code you did not open: the caller in another module that still passes the old argument list, the service in another repository that depends on the shape it returns, the test that pins the current behavior. Answer that question well and the change is routine. Answer it by guessing and you ship a green build that breaks something three hops away.

Three questions hiding inside one

"What breaks if I change this function" sounds like one question. It is three, and they have different answers.

The first is who calls it: the direct call sites, the functions that name getUser and hand it arguments. The second is what depends on its behavior: not just the direct callers but whatever sits behind them and relies on the value flowing through, a dependency that never mentions your function by name. The third is what covers it: the tests that would catch the regression if you got it wrong, and whether they actually exercise the path you are about to touch.

A rename usually needs only the first. A signature change like adding tenantId, or a change to what a function returns, needs all three. Most tools answer the first, approximately, and leave the other two to the person or the agent making the edit.

Why find-references stops short

Every IDE has a find-references command, and it is genuinely better than grep: it resolves symbols instead of matching strings, so it skips the comment that mentions the name and finds the call written across two lines. For a human refactoring inside one project with the whole thing loaded in the editor, it is often enough.

It stops short in three places. It answers who calls getUser but not what depends on the behavior two hops out, so a transitive break stays invisible until it fails. It sees the project the editor has open, so a caller living in a separate repository that consumes this code as a dependency is off the map. And an autonomous agent cannot invoke it at all: the IDE index lives behind the editor UI, not behind a callable interface the agent can hit mid-run. So the agent falls back to grep and pattern-matching, which is where the misses start, and it is the same class of gap we walk through in why AI coding agents fail on large codebases.

The agent changed the stakes

Impact analysis is not new. Static-analysis tools have walked dependency graphs for years and printed reports a human architect reads before a big refactor. That loop assumes a person in the middle who studies the report, weighs it, and decides.

An agent removes the person from the middle. It reads the question, makes the edit, and moves to the next one, dozens of times in a session. There is no pause where someone reviews an impact report. So the answer to "what breaks" has to arrive as a callable primitive the agent hits before it writes the edit, in the time of one more tool call, or it never gets consulted. A report nobody opens is worse than useless here: it is latency with a false sense of safety attached.

A getUser() node on the left connects by three labeled edges to three cards: who calls it, resolving 14 sites including one routed through an interface; what depends on its behavior, the transitive and cross-repo blast radius; and what tests cover it, six tests from list_tests_for
The three questions hiding inside "what breaks if I change getUser": its callers, the transitive and cross-repo dependents of its behavior, and the tests that actually cover it. Counts in the diagram illustrate the worked example rather than measuring a specific repository. Link to this diagram Open full size

Answering it in one call

Symvanta turns each of the three questions into a call an agent makes over MCP against a graph built by parsing your code.

For who calls it, relate with kind: "callers" returns the resolved call sites for getUser, including the one routed through an interface that grep never finds, with file paths and line bounds the agent hands straight to its editor. For what depends on the behavior, kind: "blast_radius" walks past the direct callers into the transitive set and, where repositories are linked in the same graph, across the repo boundary into every consumer. For what covers it, list_tests_for returns the tests that exercise the symbol, so the agent knows whether a safety net exists before it changes the thing under the net.

The mechanics of that transitive and cross-repo walk are their own subject, and we cover them in blast radius analysis. The short version: a call becomes an edge because the graph is built from parsed calls and implementations, so the callers you get back are the real set, resolved through interfaces, with no comments and no false positives. It reads the same whether the agent driving it is Claude Code, Cursor, or any other MCP client.

Where a parsed graph stops

A parsed graph sees what the source says. It does not see everything the program does at runtime. A method resolved by string key from a dependency-injection container, a handler invoked by reflection from a config value, a plugin loaded by scanning a directory: these are real edges in the running system that a static graph can miss or show only in part. A tool worth trusting gets the ordinary cases right, plain calls and interfaces with no false negatives, and states plainly which dynamic cases it cannot fully resolve. Merging runtime signal, traces and logs, back into the static graph is how that last gap closes.

One related trap is worth naming: reaching for semantic search to answer this. Similarity ranks how much two snippets resemble each other, a different signal from which symbol calls which, and it will return code that looks related but sits nowhere in the call path. We draw that line in code embeddings vs code graph.

"What breaks if I change this function" is the question your agent should ask before every non-trivial edit, and the one it currently guesses at. Book a 15-minute demo to watch it answered against your own callers, your own interfaces, and your own repositories.

See Symvanta on your own codebase →