{"slug": "the-tests-passed-the-architecture-didn-t", "title": "The Tests Passed. The Architecture Didn't.", "summary": "AI coding agents can produce changes that pass TypeScript, tests, and builds yet still violate a project's intended architecture, according to a first-person account of working with coding agents on larger changes. The author reports that agents bypass existing layers, add redundant implementations, and point dependencies in the wrong direction because the repository does not make intended rules explicit, and argues that deterministic checks and MCP should enforce those boundaries instead of relying on a second LLM to review diffs.", "body_md": "Why AI coding agents introduce architectural debt despite passing tests, and how deterministic checks and MCP keep projects clean.\n\n*What I started noticing after AI coding agents began making bigger changes to my projects.*\n\nOne of the stranger things about working with coding agents is that a change can be completely valid and still be wrong.\n\nTypeScript passes.\n\nThe tests pass.\n\nThe build is green.\n\nThe feature works.\n\nAnd I can still look at the diff and think: I don't want this in the codebase.\n\nI 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.\n\nThe problem wasn't usually a bug.\n\nIt was the shape of the change.\n\nA 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.\n\nNone of those things necessarily show up in a test suite.\n\nThat bothered me more than I expected.\n\nI like tests. This isn't an argument against them.\n\nA test can tell me that creating a subscription works.\n\nIt can tell me that an unauthorized user gets a 403.\n\nIt can tell me that a background job retries after a failure.\n\nThose are important questions.\n\nBut imagine the project has gradually settled on a structure like this:\n\n```\nUI\n ↓\nActions\n ↓\nServices\n ↓\nRepositories\n ↓\nDatabase\n```\n\nThen an agent adds a new feature and imports the database client directly into a UI-facing module.\n\nThe code may compile.\n\nThe feature may work.\n\nThe tests may all pass.\n\nThe problem is simply that the new code doesn't belong there.\n\nThe repository now has another path through the system.\n\nOver time those exceptions accumulate.\n\nThat's where things get expensive.\n\nWhen I write a new feature myself, I already have a rough model of the project in my head.\n\nI know which directory handles a particular kind of logic.\n\nI know that a certain integration goes through an adapter.\n\nI know that a particular operation has to go through a service because some permissions are checked there.\n\nI know that a module is supposed to be removable.\n\nI don't necessarily write any of that down.\n\nI just remember it.\n\nAn agent doesn't have that memory.\n\nIt has the repository.\n\nThat's not quite the same thing.\n\nIt can search the code and find examples, but examples aren't always rules.\n\nIf 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.\n\nIn fact, the existing inconsistency can become new training data for the next change.\n\nThat's the part I started thinking about.\n\nOnce an agent is doing a meaningful part of the implementation, architecture is no longer just something humans discuss during design.\n\nIt becomes part of the agent's working environment.\n\nThe question changes from:\n\nCan the agent write this feature?\n\nTo:\n\nCan the agent write this feature without changing how the rest of the project is supposed to work?\n\nThose aren't the same thing.\n\nA coding agent can be very good at finding all the places that need changing.\n\nIt can also be very good at making a new abstraction when an existing one would have been enough.\n\nThat's not necessarily because the model is bad.\n\nThe repository often doesn't make the intended rules explicit.\n\nMy first instinct was to think about adding another model to the process.\n\nThe agent writes the change.\n\nA second model reviews the diff.\n\nIt points out architectural problems.\n\nThe first agent fixes them.\n\nThat can work.\n\nBut the more I thought about it, the less attractive it became for some classes of problems.\n\nIf the repository tells us that one module depends on another, we don't need an LLM to determine that fact.\n\nIf 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.\n\nIf a file changed, we can determine what it imports, what imports it, which routes depend on it, and which capabilities are connected to it.\n\nThose are facts.\n\nI started wondering how much of this could be moved out of the model entirely.\n\nThat led me to a fairly important design decision.\n\nI didn't want an architecture tool that arrived with its own idea of what a \"good\" TypeScript project should look like.\n\nThere are plenty of reasonable architectures.\n\nA random project might use:\n\n```\ncomponents\nservices\nrepositories\n```\n\nAnother might use:\n\n```\nfeatures\ndomain\ninfrastructure\n```\n\nAnother might be completely different.\n\nI don't want to tell the project which one is correct.\n\nI want to start by looking at what the project actually does.\n\nThat means extracting facts from the repository:\n\nThe distinction is subtle but important.\n\nThere is a big difference between:\n\n\"This is the architecture you should use.\"\n\nand:\n\n\"This is how your project currently works.\"\n\nThe second one gives you something to work with without imposing a framework on every repository.\n\nFacts alone aren't enough.\n\nSuppose a project currently has this dependency:\n\n```\nApplication\n    ↓\nService\n    ↓\nRepository\n```\n\nThat doesn't automatically mean somebody should never be allowed to bypass the service.\n\nMaybe there is a legitimate exception.\n\nSo the next step is explicit policy.\n\nThe developer decides which observed patterns matter enough to enforce.\n\nFor example:\n\n``` python\nUI code must not import persistence code directly.\n```\n\nor:\n\n```\nAll payment-provider access must pass through the payment adapter.\nModule A cannot depend on module B.\n```\n\nThe important part for me is that these rules come from the project rather than from the tool's preferred architecture.\n\nThe project can decide what to enforce.\n\nThe tool can discover the facts and verify the decision.\n\nThere can already be violations in a real codebase.\n\nIf a project has existed for a few years, I'm not going to pretend everything was perfect before today.\n\nThat's why I find the idea of a baseline useful.\n\nI care much more about:\n\nDid this change introduce a new architecture violation?\n\nthan:\n\nDoes this repository contain any violation at all?\n\nThat distinction makes the tool much more practical for an existing project.\n\nYou can gradually tighten things instead of trying to make a messy codebase perfect before you can use the tooling.\n\nImagine this rule exists:\n\n``` python\nClient-facing modules cannot import persistence modules.\n```\n\nAn agent adds:\n\n``` js\nimport { db } from '@/lib/db';\n```\n\nto a module that sits on the client side of the application.\n\nNothing necessarily crashes.\n\nThere might not even be an obvious runtime error.\n\nBut the dependency is now different from the one the project intended to maintain.\n\nA deterministic checker can inspect the change and tell you that.\n\nThere is no need for a model to decide whether the import \"looks suspicious.\"\n\nIt's a fact that can be checked.\n\nThis is where the distinction between context and verification becomes useful.\n\nThe agent should still do the interesting work.\n\nIt can interpret a requirement.\n\nIt can decide which existing parts of the application are relevant.\n\nIt can choose an implementation.\n\nIt can reason about trade-offs.\n\nI'm not trying to replace that.\n\nI want the environment around it to answer questions that don't need model reasoning.\n\nSomething more like:\n\n```\nHuman\n  ↓\nRequirement\n  ↓\nAI agent\n  ↓\nImplementation\n  ↓\nDeterministic project checks\n  ↓\nAgent fixes violations\n  ↓\nVerification\n  ↓\nHuman review\n```\n\nThe model is still doing most of the work.\n\nThe difference is that it isn't the final authority on whether the change fits the repository.\n\nI've become more interested in MCP for development for the same reason.\n\nThere is a big difference between giving an agent another command and giving it structured information about the project.\n\nA command like:\n\n```\nrun tests\n```\n\nis useful.\n\nBut something like:\n\n```\nwhat depends on this module?\nwhat project rules apply here?\nwhat capabilities are connected to this change?\nwhat contracts does this area expose?\nwhat changed since the last accepted state?\n```\n\nstarts to become much more interesting.\n\nThe agent doesn't need to rediscover the entire repository every time.\n\nIt can ask for a specific piece of project state.\n\nI've been exploring this in Codapult's own developer tooling, where the CLI and MCP layer expose structured information about a Codapult project.\n\nThat work ended up influencing how I thought about the more general problem.\n\nI ended up building Codapult Guard as an open-source experiment around this idea.\n\nThe goal is deliberately narrower than \"AI reviews your code.\"\n\nGuard is interested in the part that can be derived and checked deterministically.\n\nIt 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.\n\nIt also looks at impact and project relationships rather than treating every changed file as an isolated blob of text.\n\nThere is no LLM call required for the core checks.\n\nThat's important to me.\n\nI'm not trying to compete with an AI reviewer by making another AI reviewer.\n\nI'd rather remove the model from questions that are already answerable from the repository.\n\nThis isn't a magic solution.\n\nA deterministic architecture check can tell me that a dependency crosses a boundary.\n\nIt can't tell me whether the business decision behind that boundary was a good one.\n\nIt can't decide whether a new product requirement makes the existing architecture obsolete.\n\nIt can't replace a human looking at a risky change.\n\nAnd a project can have a perfectly consistent architecture that is still the wrong architecture.\n\nI'm okay with that.\n\nThe point isn't to automate every decision.\n\nIt's to stop using a model where a simpler mechanism is enough.\n\nThe interesting question for me now isn't really whether agents can write software.\n\nThey obviously can.\n\nIt's how much of the software environment should be made explicit once they start doing a large part of the implementation.\n\nSome things belong in instructions.\n\nSome belong in tests.\n\nSome belong in static analysis.\n\nSome are better represented as project facts.\n\nSome need a human.\n\nAnd some really do need a model.\n\nI don't think we need to choose one of those.\n\nWe need to get better at separating them.\n\nThe more I use agents, the less I think of the repository as just source code.\n\nIt is also a record of architectural decisions, conventions, dependencies and constraints.\n\nHumans can keep a lot of that information in their heads.\n\nAn agent can't.\n\nSo 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.\n\nNot necessarily more documentation.\n\nNot necessarily another AI reviewer.\n\nSometimes just a fact that can be checked.\n\nThat's the direction I'm exploring with Codapult Guard.\n\nCodapult 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](https://codapult.dev/docs/developer-tools/mcp-server) or start building with Codapult.", "url": "https://wpnews.pro/news/the-tests-passed-the-architecture-didn-t", "canonical_source": "https://codapult.dev/blog/the-tests-passed-the-architecture-didnt", "published_at": "2026-09-22 00:00:00+00:00", "updated_at": "2026-09-23 06:54:20.774939+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "agent-protocols", "developer-tools"], "entities": ["TypeScript", "MCP"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-tests-passed-the-architecture-didn-t", "markdown": "https://wpnews.pro/news/the-tests-passed-the-architecture-didn-t.md", "text": "https://wpnews.pro/news/the-tests-passed-the-architecture-didn-t.txt", "jsonld": "https://wpnews.pro/news/the-tests-passed-the-architecture-didn-t.jsonld"}}