TL;DR
An AI‑driven code edit can pass all local checks yet explode downstream because the agent searches for similar snippets, not for the structural callers that depend on the edited file. Conventional review surfaces only the diff, not the transitive blast radius. The fix is a dependency‑aware layer that maps call‑graphs before any change is merged.
The 3 a.m. incident: a single middleware tweak that broke three services
It was Wednesday, 02:17 AM. The on‑call engineer woke to a Slack ping: “Production is returning 401 from three unrelated services.” The stack consisted of:
| Service | Repo | Entry point |
|---|---|---|
| Auth‑gateway (Node/Express) | auth-gateway/ | src/middleware/auth.js |
| Payments API | payments/ | src/routes/pay.js |
| User‑profile API | profile/ | src/routes/profile.js |
| Analytics collector | analytics/ | src/collectors/event.js |
All four repos shared a tiny auth‑middleware utility located at shared-lib/auth-utils.js. The file exports a single helper, verifyToken(token), that validates a JWT and returns the decoded payload.
An AI coding agent (triggered from a pull‑request comment) suggested a refactor: replace the custom jwt.verify wrapper with the library’s built‑in jwt.decode for a “speed win.” After the agent ran the repository’s unit test suite where all 317 tests passed, the PR was merged. Two days later, the three downstream services started throwing TokenExpiredError at runtime, despite none of their source files appearing in the diff.
The change is three lines, each of which passes the local test harness because the suite only checks that a well‑formed token yields a payload. The real production traffic includes expired or tampered tokens that now slip through, causing downstream services to reject the request later in the pipeline. The breakage is invisible in the diff, yet it is the direct cause of the outage.
Why this keeps happening: agents retrieve “similar” code, not “connected” code
Most production‑grade AI coding agents operate in two stages:
- Context retrieval: they embed every file in the repo (or a large chunk of the codebase) into a high‑dimensional vector space.
- Similarity search: given a natural‑language prompt, they pull the k nearest vectors and feed those snippets to the LLM.
| Retrieval method | What it captures | What it ignores |
|---|---|---|
| Embedding similarity | Lexical tokens, identifier names, comment phrasing | Call‑graph edges, import/export relationships, runtime data flow |
| Call‑graph analysis | Function callers, module dependencies, transitive imports | Semantic similarity of identifiers, code style |
Embedding similarity is low-cost and works well for local refactors (e.g., “rename this variable”). It fails when the impact of a change is defined by structural connections rather than textual likeness. The auth‑middleware edit above is a perfect illustration: the function name verifyToken appears in many places, but the call‑graph that links it to downstream services lives in separate repos that never surface in the top‑k similar snippets.
Because the retrieval step never surfaces those dependent modules, the LLM never “sees” the downstream effect when it generates the edit. The model therefore assumes the change is safe, and the downstream breakage only appears later, when the edited code is executed in a different context.
The core distinction: similarity vs. structure
Similarity graph
Nodes = code fragments (files, functions, classes).
Edges = cosine similarity above a threshold in embedding space.
- Properties: dense, fuzzy, symmetric.
- Use case: surface “examples that look like this” for autocomplete or documentation generation.
Structure (dependency) graph
Nodes = same code fragments.
Edges = static or dynamic analysis links (import, function call, data flow).
- Properties: directed, often sparse, reflects execution order.
- Use case: impact analysis, build tooling, runtime tracing.
In the similarity graph, auth-utils.js may be close to files that also import jsonwebtoken, regardless of whether they call verifyToken. In the structure graph, there is a directed edge from each downstream service’s route handler to verifyToken. That edge is the only path that conveys the blast radius.
The series will repeatedly return to this distinction because every failure mode we discuss can be traced to a missing edge in the structure graph.
Why conventional code review doesn’t catch it
A typical pull‑request review looks like:
- Diff view: shows added/removed lines.
- File list: limited to files touched by the commit.
- Static analysis warnings: confined to the changed files.
What the reviewer does not see:
| Missing view | Reason |
|---|---|
| Transitive callers up the import chain | Not part of the changed file set |
| Runtime data‑flow that validates tokens later | Requires execution or a full call‑graph |
| Cross‑repo dependencies | PR usually lives in a single repo; external services are invisible |
In the auth‑middleware example, the diff is three lines, the reviewer can verify that the LLM didn’t introduce a syntax error, and the CI pipeline reports green. The reviewer never sees:
payments/routes/pay.js → auth-utils.verifyTokenprofile/routes/profile.js → auth-utils.verifyTokenanalytics/collectors/event.js → auth-utils.verifyToken
Because those edges are outside the PR, the reviewer cannot reason about the downstream impact. The result is a false sense of safety that matches the observed pattern of “the change looks harmless, but production blows up.”
What actually has to change: a call‑graph‑aware guard layer
To stop the blind‑spot, the edit pipeline needs an intermediary that:
- Builds a live dependency graph for the entire monorepo (or federated set of repos).
- Maps the proposed edit to the set of downstream nodes that would be affected.
- Fails the edit (or flags it) if the blast radius exceeds a policy threshold.
Minimal architecture
| Component | Responsibility |
|---|---|
| Static analyzer | Generates an import‑call graph using tools like ts-morph, eslint‑scope, or language‑server protocols. |
| Change detector | Takes the diff, identifies modified symbols, and looks them up in the graph. |
| Impact evaluator | Traverses the graph downstream, aggregates affected files/services, compares against a risk matrix (e.g., “touches authentication → high risk”). |
| Policy enforcer | Emits a reject or a mandatory review flag; optionally auto‑generates a “blast‑radius report.” |
Pseudocode sketch
The key point is the guard runs before the edit is merged. The AI agent still generates the change, but the pipeline refuses to ship it unless the impact is explicitly acknowledged. This turns an accidental blast‑radius event into a visible risk that can be triaged.
CodeFundi: mapping blast radius before the edit ships
CodeFundi provides the missing dependency layer out of the box. It continuously indexes a repository’s call‑graph, then, on every PR, returns a blast‑radius report that lists every downstream file, service, and data‑flow path that would be touched by the proposed change. The report can be attached to the PR, used as a gating condition in CI, or fed back to the AI agent so it can rewrite its suggestion with the full impact in mind.
FAQ
Why does my AI coding assistant break unrelated files?
Because the assistant’s retrieval step is based on embedding similarity, which does not capture the call‑graph edges that connect the edited symbol to downstream code. The assistant never “sees” the dependent files, so it can’t reason about the downstream effect.
Do Cursor and Copilot understand code dependencies?
Both tools excel at surface‑level pattern matching and generate edits based on textual similarity. They do not natively perform static call‑graph analysis, so they share the same blind spot described above.
What is blast radius in software engineering?
Blast radius is the set of code locations (files, functions, services) that will be affected when a particular change is deployed. It is a structural property derived from the dependency graph, not a measure of how many lines were edited.
Next step: Curious how large a blast radius a single edit can have in your own codebase? Try the live demo and map your own repo's blast radius.