Codapult
DocsBlogPricingPluginsDemo
Get Codapult
Logo

Build your SaaS with AI. Keep the architecture under control.

Get Codapult

Product

  • Pricing
  • Architecture
  • Modules
  • AI platform
  • Plugins

Developers

  • MCP
  • CLI
  • Documentation

Resources

  • Blog
  • FAQ

Connect

  • Contact
  • GitHub

Compare

  • SaaS template comparison
  • Codapult vs Supastarter
  • Codapult vs Makerkit
  • Codapult vs ShipFast
  • Codapult vs SaaSBold
  • Codapult vs Gravity
  • Codapult vs Nextbase
  • Codapult vs BuilderKit

Featured on

Codapult on LaunchNestCodapult on LaunchNestbetterlaunch.cobetterlaunch.coFeatured on LaunchBuffFeatured on LaunchBuffCodapult on PeerPushCodapult on PeerPushFeatured on LaunchItFeatured on LaunchIt
© 2026 Codapult·All rights reserved·Privacy Policy·Terms of Service
Full source code · One-time purchase · Self-host anywhere
Back to blog
September 22, 2026·11 min read·Codapult Team

The Tests Passed. The Architecture Didn't.

Why AI coding agents introduce architectural debt despite passing tests, and how deterministic checks and MCP keep projects clean.

ai-agentsarchitecturemcpdeveloper-toolstypescript

The Tests Passed. The Architecture Didn't.

What I started noticing after AI coding agents began making bigger changes to my projects.

One of the stranger things about working with coding agents is that a change can be completely valid and still be wrong.

TypeScript passes.

The tests pass.

The build is green.

The feature works.

And I can still look at the diff and think: I don't want this in the codebase.

I started running into this more often as I let agents handle larger changes instead of asking them to implement one small function at a time.

The problem wasn't usually a bug.

It was the shape of the change.

A new module would bypass an existing layer. A feature would introduce another way of doing something the project already had. A dependency would point in the wrong direction. A new piece of code would technically work but ignore a convention that had become important elsewhere.

None of those things necessarily show up in a test suite.

That bothered me more than I expected.

Tests answer a different question

I like tests. This isn't an argument against them.

A test can tell me that creating a subscription works.

It can tell me that an unauthorized user gets a 403.

It can tell me that a background job retries after a failure.

Those are important questions.

But imagine the project has gradually settled on a structure like this:

UI
 ↓
Actions
 ↓
Services
 ↓
Repositories
 ↓
Database

Then an agent adds a new feature and imports the database client directly into a UI-facing module.

The code may compile.

The feature may work.

The tests may all pass.

The problem is simply that the new code doesn't belong there.

The repository now has another path through the system.

Over time those exceptions accumulate.

That's where things get expensive.

This is easier to notice when a human writes the code

When I write a new feature myself, I already have a rough model of the project in my head.

I know which directory handles a particular kind of logic.

I know that a certain integration goes through an adapter.

I know that a particular operation has to go through a service because some permissions are checked there.

I know that a module is supposed to be removable.

I don't necessarily write any of that down.

I just remember it.

An agent doesn't have that memory.

It has the repository.

That's not quite the same thing.

It can search the code and find examples, but examples aren't always rules.

If it finds three slightly different implementations, it doesn't automatically know which one is the accidental old implementation and which one represents the intended pattern.

In fact, the existing inconsistency can become new training data for the next change.

That's the part I started thinking about.

The architecture becomes part of the input

Once an agent is doing a meaningful part of the implementation, architecture is no longer just something humans discuss during design.

It becomes part of the agent's working environment.

The question changes from:

Can the agent write this feature?

To:

Can the agent write this feature without changing how the rest of the project is supposed to work?

Those aren't the same thing.

A coding agent can be very good at finding all the places that need changing.

It can also be very good at making a new abstraction when an existing one would have been enough.

That's not necessarily because the model is bad.

The repository often doesn't make the intended rules explicit.

I didn't want another AI reviewer

My first instinct was to think about adding another model to the process.

The agent writes the change.

A second model reviews the diff.

It points out architectural problems.

The first agent fixes them.

That can work.

But the more I thought about it, the less attractive it became for some classes of problems.

If the repository tells us that one module depends on another, we don't need an LLM to determine that fact.

If a project has explicitly approved a dependency boundary, we don't need a model to have an opinion about whether a particular import crosses it.

If a file changed, we can determine what it imports, what imports it, which routes depend on it, and which capabilities are connected to it.

Those are facts.

I started wondering how much of this could be moved out of the model entirely.

Start with the project that already exists

That led me to a fairly important design decision.

I didn't want an architecture tool that arrived with its own idea of what a "good" TypeScript project should look like.

There are plenty of reasonable architectures.

A random project might use:

components
services
repositories

Another might use:

features
domain
infrastructure

Another might be completely different.

I don't want to tell the project which one is correct.

I want to start by looking at what the project actually does.

That means extracting facts from the repository:

  • files and directories
  • imports and dependency relationships
  • AST information
  • routes
  • capabilities
  • contracts
  • change impact
  • other relationships that can be derived from the code itself

The distinction is subtle but important.

There is a big difference between:

"This is the architecture you should use."

and:

"This is how your project currently works."

The second one gives you something to work with without imposing a framework on every repository.

Then make the rules explicit

Facts alone aren't enough.

Suppose a project currently has this dependency:

Application
    ↓
Service
    ↓
Repository

That doesn't automatically mean somebody should never be allowed to bypass the service.

Maybe there is a legitimate exception.

So the next step is explicit policy.

The developer decides which observed patterns matter enough to enforce.

For example:

UI code must not import persistence code directly.

or:

All payment-provider access must pass through the payment adapter.

or:

Module A cannot depend on module B.

The important part for me is that these rules come from the project rather than from the tool's preferred architecture.

The project can decide what to enforce.

The tool can discover the facts and verify the decision.

The useful check is usually about the change

There can already be violations in a real codebase.

If a project has existed for a few years, I'm not going to pretend everything was perfect before today.

That's why I find the idea of a baseline useful.

I care much more about:

Did this change introduce a new architecture violation?

than:

Does this repository contain any violation at all?

That distinction makes the tool much more practical for an existing project.

You can gradually tighten things instead of trying to make a messy codebase perfect before you can use the tooling.

A simple example

Imagine this rule exists:

Client-facing modules cannot import persistence modules.

An agent adds:

import { db } from '@/lib/db';

to a module that sits on the client side of the application.

Nothing necessarily crashes.

There might not even be an obvious runtime error.

But the dependency is now different from the one the project intended to maintain.

A deterministic checker can inspect the change and tell you that.

There is no need for a model to decide whether the import "looks suspicious."

It's a fact that can be checked.

What I actually want the agent to do

This is where the distinction between context and verification becomes useful.

The agent should still do the interesting work.

It can interpret a requirement.

It can decide which existing parts of the application are relevant.

It can choose an implementation.

It can reason about trade-offs.

I'm not trying to replace that.

I want the environment around it to answer questions that don't need model reasoning.

Something more like:

Human
  ↓
Requirement
  ↓
AI agent
  ↓
Implementation
  ↓
Deterministic project checks
  ↓
Agent fixes violations
  ↓
Verification
  ↓
Human review

The model is still doing most of the work.

The difference is that it isn't the final authority on whether the change fits the repository.

This also changes how MCP becomes useful

I've become more interested in MCP for development for the same reason.

There is a big difference between giving an agent another command and giving it structured information about the project.

A command like:

run tests

is useful.

But something like:

what depends on this module?
what project rules apply here?
what capabilities are connected to this change?
what contracts does this area expose?
what changed since the last accepted state?

starts to become much more interesting.

The agent doesn't need to rediscover the entire repository every time.

It can ask for a specific piece of project state.

I've been exploring this in Codapult's own developer tooling, where the CLI and MCP layer expose structured information about a Codapult project.

That work ended up influencing how I thought about the more general problem.

That's why I built Codapult Guard

I ended up building Codapult Guard as an open-source experiment around this idea.

The goal is deliberately narrower than "AI reviews your code."

Guard is interested in the part that can be derived and checked deterministically.

It builds a model of a JavaScript or TypeScript project, lets the developer turn relevant project facts into explicit policy, and can then inspect changes against that policy.

It also looks at impact and project relationships rather than treating every changed file as an isolated blob of text.

There is no LLM call required for the core checks.

That's important to me.

I'm not trying to compete with an AI reviewer by making another AI reviewer.

I'd rather remove the model from questions that are already answerable from the repository.

There are obvious limitations

This isn't a magic solution.

A deterministic architecture check can tell me that a dependency crosses a boundary.

It can't tell me whether the business decision behind that boundary was a good one.

It can't decide whether a new product requirement makes the existing architecture obsolete.

It can't replace a human looking at a risky change.

And a project can have a perfectly consistent architecture that is still the wrong architecture.

I'm okay with that.

The point isn't to automate every decision.

It's to stop using a model where a simpler mechanism is enough.

The part I'm still figuring out

The interesting question for me now isn't really whether agents can write software.

They obviously can.

It's how much of the software environment should be made explicit once they start doing a large part of the implementation.

Some things belong in instructions.

Some belong in tests.

Some belong in static analysis.

Some are better represented as project facts.

Some need a human.

And some really do need a model.

I don't think we need to choose one of those.

We need to get better at separating them.

One last thing

The more I use agents, the less I think of the repository as just source code.

It is also a record of architectural decisions, conventions, dependencies and constraints.

Humans can keep a lot of that information in their heads.

An agent can't.

So if we want agents to make larger changes without slowly turning every project into a collection of locally reasonable decisions, some of that knowledge needs to become explicit.

Not necessarily more documentation.

Not necessarily another AI reviewer.

Sometimes just a fact that can be checked.

That's the direction I'm exploring with Codapult Guard.

Codapult Guard on GitHub


Codapult is built from the ground up as an AI-native SaaS foundation. It ships with a built-in Model Context Protocol (MCP) server, AST-based health checks, and clean architectural boundaries so AI agents in Cursor, Claude Desktop, and Windsurf can build and operate your SaaS without corrupting your codebase. Explore the developer tools in the MCP server docs or start building with Codapult.