Why a dependency graph only shows current code relationships, and why AI coding agents need explicit architectural policy to avoid technical debt.
A graph can show how your code is connected. It can't tell you which of those connections your project actually wants to keep.
A dependency graph is one of the first things I wanted when I started thinking about tooling for AI coding agents.
It makes sense.
An agent needs to understand a codebase, and a graph is a much better representation of relationships than a pile of files.
You can see that a route depends on a service. That service depends on a repository. A repository depends on a database client. Another module imports the service. A shared utility is suddenly used by half the application.
That information is useful.
But after working with this problem for a while, I started thinking that there is a trap here.
A dependency graph is not architecture.
It is a description of relationships. Architecture also contains intent.
Suppose I have this:
Client
↓
Server Action
↓
Service
↓
Repository
↓
Database
A graph can represent that pretty well.
It can tell me that the service imports the repository. It can tell me that the repository imports the database client. It can tell me which files are connected to a changed module. It can show cycles or unusually large dependency hotspots.
All of that is valuable.
Now imagine another part of the application has this:
Client
↓
API Route
↓
Database
The graph can show that too.
What it cannot tell me by itself is whether the second path is wrong.
The graph contains all four possibilities. It has no way to choose between them.
This is where the difference matters.
Architecture isn't only a collection of edges between components. It also contains decisions like:
Those are not just observations about the current repository. They are decisions. And some of them can exist even when the repository doesn't currently satisfy them.
That's an important distinction.
The code might currently contain:
Checkout → Stripe
while the intended architecture is:
Checkout → Payment Adapter → Stripe
If all you have is the current graph, the direct dependency is simply a fact. You don't know that it is a violation until someone tells you what is supposed to be true.
I've seen a few different approaches to this problem.
One is to infer architecture from directory names. If there is a services directory, call it the service layer. If there is a repositories directory, assume everything under it is persistence.
That works until it doesn't.
Another project might use:
features/
domain/
infrastructure/
Another might put all of its business logic inside feature folders. Another might have very little layering at all. Another might have started with one architecture and gradually evolved into something else.
None of those structures is automatically correct.
So I don't really want a tool to look at a repository and say: "This is good architecture."
That seems like the wrong question. I'd rather ask:
Those are much more concrete questions.
There is another problem. Real repositories contain history. Not all history is intentional.
A developer copies an implementation. Another developer adds a shortcut. Someone moves a file but keeps the old import. A feature gets rushed into production. Six months later there are three different ways to access the same thing.
The graph faithfully records all of them.
In that sense, a graph can actually make the ambiguity more visible without resolving it. You might have:
Feature A → Service A → Repository
Feature B → Repository
Feature C → Service C → Repository
Feature D → Service A
What should a coding agent learn from this?
It could conclude that both direct repository access and service access are valid patterns. It could pick the shortest path. It could copy the most common one. It could look at recent commits.
None of those decisions necessarily tells you what the project intended.
And if an agent starts using an accidental pattern as an example for new work, the inconsistency becomes self-reinforcing. That's a much bigger problem than just having an ugly graph.
This is the part I keep coming back to. A useful project model has at least two different things:
The graph is very good at the first one. It doesn't magically give you the second.
For example:
Facts:
Checkout imports PaymentAdapter
PaymentAdapter imports Stripe
RefundService imports Stripe
And then:
Policy:
All payment-provider access must go through PaymentAdapter
Now the system can check something meaningful. A direct Stripe import isn't merely "an unusual edge." It is a concrete violation of a known project decision.
That's a much stronger signal for an AI agent too.
Architectural rules almost always have exceptions.
Suppose the rule is: UI modules must not import persistence modules.
Then someone builds an internal admin export screen. The simplest implementation might legitimately need a different path.
Without an explicit policy model, you tend to end up with one of two bad outcomes:
An explicit policy gives you somewhere to put the exception. The project can say:
That is much closer to how real architecture works.
The same thing happens with existing violations.
Imagine a large application already has twenty direct database imports from places that ideally shouldn't have them. The graph can find twenty edges. A naive architecture checker might immediately fail the project.
That's not especially useful. I don't need a tool telling me that yesterday's codebase was imperfect. I need it to help me stop making the situation worse.
So a more useful question is:
That makes the graph part of a useful workflow instead of turning it into a giant list of problems.
This becomes more important when the repository is being modified by an AI coding agent.
An agent can query a graph and ask:
That's great. But then it needs another question: Am I allowed to make this dependency?
The first question can be answered from repository facts. The second requires policy.
If the system returns: "This import exists," that's one kind of information.
If it returns: "This import exists. This dependency crosses an approved boundary," that's much more useful. It gives the model something it can act on.
Git history is useful, but I don't think history should be treated as policy either.
Suppose the agent sees that a particular dependency was added in a large refactor eighteen months ago. That tells you something. Maybe there was a reason. But it doesn't necessarily tell you whether the dependency is still intentional.
The same applies to code frequency. A pattern used 200 times is not automatically a good pattern. A pattern used once is not automatically wrong.
History can provide evidence.
It shouldn't silently become a rule.
This is one of the ideas that shaped Codapult Guard.
I didn't want to build another system that looks at a repository graph and declares what the architecture should be. Guard starts with the repository that already exists. It discovers facts about the project: files, imports, AST relationships, routes, capabilities, contracts, and other relationships derived from the code.
Then the project can decide which of those things should become policy.
After that, the useful part is verification. For example:
Client module → Database import
can be reported because the project explicitly decided that this dependency isn't allowed.
Guard doesn't treat every possible architectural preference as a universal rule. The project decides, the tooling checks, and existing violations can be baselined so future changes are checked cleanly.
The core checks are deterministic and local. There is no need to ask another model whether one module imports another.
None of this means graphs are overrated. Quite the opposite.
A graph is excellent for:
Policy can answer:
Verification can answer:
Putting them together is much more powerful than pretending one representation can answer all of them.
My first instinct was always: give the agent more information. More files, more history, more dependencies, more documentation, more graph data.
But eventually you hit diminishing returns. The agent doesn't necessarily need the whole graph. It needs the facts that are relevant to the decision it is making, and it needs to know which facts are constraints rather than observations.
For a change to a payment module, I probably don't need a thousand unrelated edges. I need:
That's much closer to useful project context.
The shape is the visible part. The decisions are the interesting part.
A graph can preserve the shape. Policy can preserve some of the decisions. History can provide evidence about how we got there.
Combining them gives an agent a model of the system that separates:
what is true
↓
what is intended
↓
what changed
That separation is probably more important than making the graph itself smarter.
I don't think the answer is one giant "AI architecture brain" that understands everything in a repository. I'd rather have smaller pieces that do one thing reliably:
If an AI can change a hundred files in one session, the environment must tell it which parts of that context are observations, which parts are constraints, and which decisions are still open.
A dependency graph is a very good start. It just isn't the architecture.
Turn architectural intent into explicit rules for your codebase and AI agents.
Codapult isolates domain logic from infrastructure using strict adapter boundaries (Auth, Payments, Database) and deterministic policy checks. Rather than relying on implicit conventions, it exposes explicit architecture contracts and MCP resources so both developers and AI agents know what connections are allowed before making a change. Explore the policy boundaries in the Codapult Architecture Docs.