Ask a coding agent to change a component and it will do it. Ask it what that change broke and it will guess.
Up front: I co-founded Bit Cloud, which works on this. It is where the numbers come from, and I name it where it is the relevant answer. Weigh it accordingly.
Here is what that looks like in a system I maintain. One of the products is a learning platform, and the mentor you talk to inside it was never built for that platform at all: it is a separate product of mine, installed as a published component, a chat widget talking to its own retrieval service.
Now ask what breaks if I change that widget. The page that renders it does import it, so the edge is written down somewhere: in the other product’s repository, in a statement that resolves into node_modules. Working from the widget, nothing in reach says that page exists. Cursor, Claude Code and Copilot all sit in front of the same gap, whatever each of them indexes, because the information is not in any repository they can see.
This isn’t just a story about my own setup. DORA’s 2025 State of AI-assisted Software Development report describes time saved during generation being re-spent on auditing and verification, and finds around 30% of developers reporting little or no trust in the code AI writes for them. That tax has several causes; I am after one of them.
Two sets, and they are not the same set
Every code-retrieval system in production answers one question: given this query, which parts of the codebase most resemble it? Embed the chunks, embed the query, return the nearest neighbours, rerank, widen the window. Every one of those improvements makes the same output better: the set of code that resembles what you asked about.
An agent trying to change something safely needs a different set, which is the set of code that breaks when the thing it is changing changes.
Those two sets overlap, but far less than the tooling assumes. A consumer that imports a function shares its names, so retrieval usually finds it. But a theme file and a button share nothing at all, and changing the theme’s contract breaks the button anyway, so the one relationship that matters is the one an index scores lowest. It fails the other way too, handing back two form validators written months apart that look alike and are connected to nothing, which invites the agent to reconcile them.
And none of this is an accident of how some codebase grew. It is what you get when the engineering is done properly. The same system has an authentication kit in it: a neutral contract written as plain types, with a Descope implementation and a Clerk one behind it. Components depend on the contract and never on the provider, which is why swapping one for the other cost me an afternoon, and why the provider’s name appears nowhere in consumer code. Ask a similarity index what uses Descope and you get exactly one file: the provider wrapping the SDK. Every contract you put between two things deliberately removes a vocabulary overlap the index was relying on.
Resemblance is a property of text, and dependency is a property of a system. The artifact in front of you carries the first one, and it cannot carry the second, because the system it belongs to is somewhere else.
The component on the left is a build environment, which determines compilation, dependency resolution and test setup for everything using it, so a change genuinely reaches all twenty-eight. Not one of those twenty-eight contains text resembling an environment configuration, so the similarity score is correctly low and completely useless.
That is one component. The pattern holds across kinds. Here is one instance of each thing I share across these products. The design system, the product that consumes most of it and the mentor’s own scope are public, so every row but the last can be checked; that one sits in two scopes I have not opened.
|
What is shared |
A real instance |
Consumed by |
Does it resemble its consumers? |
|---|---|---|---|
|
A visual primitive |
|
8 components |
Somewhat |
|
A theme |
|
14 components |
Barely |
|
A build environment |
|
28 components |
No |
|
A running product |
|
2 pages in a different product |
No |
|
A backend service |
|
another product’s service |
No |
Read the last column downward.
The top row is what people picture when they hear “component reuse”, and retrieval handles it least badly, since a button and a page that renders buttons share vocabulary. The middle rows already break inside one product, because a hook fetching user progress shares no words with a theme or a build environment. The bottom two are the ones that matter: a running service and a live backend, not primitives. The most valuable things being reused resemble their consumers least, which is backwards from what a similarity index is good at, and they are the ones nothing else can see. Nobody reuses a service they cannot discover. They rebuild it.
Modularity breaks the import graph in two different ways
So the vocabulary is gone. Read the imports instead, which is the reasonable next move and helps less than you would think: it breaks in two ways, and both get worse the better the system is assembled.
First, the import survives but stops being followable. In a component-based codebase the import is right there in the file:
Declared, greppable, unambiguous, and it resolves into node_modules, where every local analyser gives up. That published component has its own dependencies, its own dependents and a version history, none of it on your machine. You can see the edge exists; what sits on the far end, and who else holds that end right now, you cannot.
Second, higher up, the import disappears entirely. Where whole applications and services get assembled instead of imported, there is often no statement to read: a feature registering itself into a platform slot at startup, an edge living in a config array, a deploy-time choice between two auth providers, or two components agreeing on a shape nobody declared.
One platform component in that system assembles a React application, two Node services and a gateway. Here is how it names them:
import.meta.resolve hands back a path. Nothing is imported, no type flows across, and “find all references” on the app returns nothing, because the argument is a string. Nx builds its graph from TypeScript imports, so Nx does not see this either unless someone declares the edge by hand.
Before calling that exotic, notice what it is. A real import of the frontend would pull browser code into a Node process, and importing any of them would run module side effects at load time. The platform does not consume these units, it arranges them, so it names them by identity instead of by value. Every composition root does this: webpack entry points, images: in a compose file, service references in Kubernetes, module sources in Terraform. Anyone assembling separately deployed things already has this edge, and in most stacks that string is recorded nowhere except the config file it sits in.
Here it gets resolved against the dependency graph, pinned, and written into the version record when that version is created:
That third entry, agents-4-all/agent-service, is the mentor’s backend from the opening, consumed as a package and pinned, and both ends of that edge are public if you want to check it. So the edge is not sitting in a manifest waiting to be parsed. It was computed once, when the thing was built, and stored as a fact, which is why reading it back is a lookup.
Be equally exact about what it does not carry. It declares that these units ship together, a real blast-radius signal: change one and the others are implicated. It says nothing about the frontend’s assumption regarding the service’s response shape, which is agreed over HTTP at a deploy-time address and written down nowhere.
The co-assembly edge is declared and written down. It is just not written where an indexer looks. Registration and composition both produce edges sitting in an artifact nobody indexes: unread, not unknowable. A deploy-time choice and an undeclared structural contract are in no artifact at all, and those stay properly unsolved.
What being able to ask actually bought
There was a period before this one where the graph existed and nothing could reach it, and every session I re-stated the same boundaries by hand, pulling the agent back inside an architecture that was, from its point of view, not there. It worked, it did not scale, and it was a stupid use of a person.
I’ve spent the last several months maintaining the current system on my own: a dozen or so applications and shared libraries, the largest around forty components, sharing the design system above, a typed API client, one product’s chat service, and a capture library installed from its package.
The only reason any of it was workable is that the agent could ask. Every component records what it depends on when its version is created, and those records arrive as a tool call, not as a paragraph pasted into a prompt nine turns earlier and hoped to still be relevant. Before touching anything shared it could ask what else held the other end; before writing something new, whether that thing already existed.
Here’s the shape of it over MCP. The shape of the call is generic, a tool returning recorded edges, and inside one repository a project graph answers the intra-repo half of it. The pinned versions and the scope crossings are the part it cannot:
Three of those cross a scope boundary, and as before this is a lookup, nothing embedded and nothing parsed. Run it the other way, what depends on this, and it is the same data backwards. That is where the counts here came from: I asked an agent to walk the graph.
None of this made the model smarter, but it made one category of mistake unavailable: the agent could not quietly duplicate something that already existed, and it could not change a shared contract while leaving the dependents implicit, because listing them is a lookup. Across those products that is the difference between 19 shared components carrying 97 usages and 97 separate implementations quietly drifting apart. A usage there is one component naming a component from another scope in its published record, so it counts reuse and nothing else. One person can hold the first of those; nobody holds the second at any size.
Those two asks are different queries, and conflating them is why this layer usually gets built badly. What depends on this is a traversal over recorded edges. Does something equivalent already exist is a search over declared APIs, a different index and the more valuable of the two: an agent asked to add a chat interface will build one, with no way to know a finished, deployed one exists two products over. That duplicate is not a failure of reasoning; it is the correct output given what the agent could see. Similarity search is still right for where do I start, and the mistake is asking it all three.
What changes with more than one team
The arithmetic inverts the moment more than one person is involved. In a one-person system a publication boundary is something you choose to create, and I did not want many. In a multi-team organisation every team boundary is a publication boundary by construction, so the count stops tracking anyone’s discipline and starts tracking the org chart. What fails there is visibility and not care: whoever changes a shared contract has never seen most of what holds the other end. At a dozen products and one person a queryable graph is a convenience; at several hundred components and more than one team, where nobody has seen most of the system, it stops being one.
“We have had import graphs for a decade”
A fair objection: if the edges are recorded somewhere, as I keep insisting they are, why has this not already been solved by tools that have existed for years?
Partly it has, and those tools do real work: language servers resolve references, Nx and Turborepo maintain project graphs, and for any wiring you expressed as an import inside one repository the traversal already exists. Cross-repository tools go further, with Sourcegraph resolving references across repos, GitHub showing “Used by” and npm listing dependents. Most of them ship MCP servers now, so an agent can reach them while it works, and reachability has stopped being the differentiator. What is reachable still is.
Two things differ. They index at repository or package granularity, so you get “this package is used by that product”, not “this component’s contract change breaks those two pages at this version”. And they hold the edge only when both sides were indexed, which fails for private and cross-organisation code.
Underneath both sits a third difference: these tools derive the graph by analysis at read time, from whatever is in front of them, while an edge recorded when the version was created needs no analysis and does not depend on having both sides in view.
Which brings back the boundary from earlier, from the other side. Going out, local tooling stops at node_modules. Going back, run those same tools inside the published library’s own repository and there is no record the consumer exists. Neither direction crosses, and reverse edges only exist if something records them where the publishing happens.
That is the category, and it is the one I work in. Bit Cloud records the edge at component granularity when the version is created and holds it across repositories and scopes, which is why every query here is a lookup and not an analysis. It is not the only shape that would work: anything that records at that granularity, across those boundaries, answers the same questions.
What this does not fix, and what survives it
A dependency graph does not tell you whether a change is correct, only what is affected. Knowing the blast radius is not knowing the outcome.
It does not help where the structure genuinely is not there: a codebase with no component boundaries has no graph to expose, and no recorded edge will surface a contract nobody declared.
It requires the unit of work to be something the system can record, which is a real design decision even if it is not much of a tax.
And this is one system, self-reported, with no control condition. Treat it as a field report, not a benchmark.
What survives all of that is the shape of the problem. Generation is in good shape, and models write correct code from a description more reliably than most people expected this fast. Modification is not, and the reason is not model capability. We ask a text-similarity system a question about system structure, get a poor answer, and treat it as a context-length problem. Then we draw a clean boundary, compose instead of import, and make the answer worse.
The numbers above come from systems running on what we build at Bit Cloud, and none of it requires our product: only that edges get recorded wherever the work happens, and that an agent can ask about them while it works.
If you are in one repository with ordinary imports, most of what you need is already computable and your agent is simply not asking for it. Wire those queries to whatever your language server or project graph already knows, and treat that import graph as a floor: it covers the wiring you expressed as imports and leaves you registration, deploy-time selection, and everything past your own repository.
The learning platform I opened with is still running, and its mentor is still another product. It stayed that way for one reason: something recorded the edge, so nobody had to rebuild it. Structure your agent cannot query does not constrain your agent. It only constrains you.

