Architecture rules an AI coding agent cannot quietly break

25 October 2026

Ask a coding agent to show an order's delivery status on the invoice page and it will find the shortest route to the data. Often that is an import straight from the controller into the database layer, past the service your team spent a year keeping in between. The tests pass. The reviewer, reading a forty-file diff on a Friday, may not notice one new import line. Six months later the layering exists only on the whiteboard.

A person makes this mistake occasionally. An agent makes it every time the shortcut is shorter, because nothing in the task tells it the shortcut is off limits. So we stopped relying on the prompt alone and made the rules something a run is checked against.

First, a map that is true

You cannot enforce an architecture you can only describe. So CodeLynq reads each repository with tree-sitter and records which files depend on which, and what they declare: classes, functions, fields, inheritance. It works for Python, JavaScript, TypeScript, Go, Java, C# and PHP.

Nobody edits the map by hand; it is whatever the code says. It is redrawn at the start of every run, from the clone the run already has, after a pull request merges into the default branch, every night when the branch has moved, and when someone presses Refresh. That matters for rules: a check against last quarter's diagram is a check against fiction.

Then, rules you write down

On the Architecture tab you name your components as paths, such as API, Domain and Persistence, and say which may not depend on which: the domain must not import from the database layer, the web app must not reach into the worker. A rule can carry a note explaining why, and the note travels with it.

Only an organisation admin or the project owner can change them, and every change is in the audit log. Rules that any member could relax to get a run through would not be rules.

What a run does with them

The components and rules go into the agent's instructions before it writes anything, along with the current dependencies between components. That alone prevents a share of the shortcuts. It does not prevent all of them, which is why the instructions are not the control.

The control comes after the code is written. The run maps the changed files again and compares the result with the map it took before the agent started. What it looks for is narrow on purpose: dependencies this change added that a rule forbids. Violations already in your code are not the run's doing, and failing a small bugfix because the codebase broke a rule two years ago would teach everyone to switch the check off.

Each repository picks one of three modes:

  • Fix, the default. A new violation goes back to the agent with the exact imports that break which rule, and an instruction to restructure: move the code to the component allowed to own it, depend on an abstraction, or pass the value in. Not to delete the feature, copy code to dodge the import or hide the import inside a function. If the project has a verify command, it runs again after each attempt, and the attempts are capped.
  • Report. No fix attempts; violations are listed in the pull request for a person to judge.
  • Off. The map still updates, nothing is checked.

Iterations on an open pull request are checked the same way, against the target branch, so the check covers the whole pull request rather than only the latest commit. If a violation survives an iteration, it is posted as a comment on the pull request. On a self-hosted runner, the runner sends only the source files its agent changed for the check, never the whole repository.

The gate never fails the run

This was a deliberate choice, and the same one we made for the review and security gates. A broken build should stop a run. A forbidden import that two fix attempts could not remove usually means the task itself crosses a boundary, and that is a design decision for a person, not a reason to throw away working code. What is left is stated plainly, under "Architecture rules broken by this change", with the file that imports and the file it imports.

Structure changes, in every pull request

Every pull request a run opens gets a Structure changes section, even when no rule was broken: files added and removed, new declarations, new dependencies between components (or between top-level directories, if you have not named components), new external packages, and any existing violations the change happens to resolve. Declarations in test files are left out, since a new test function is not a new part of the system.

It answers the question a reviewer actually has about a large diff: did the shape of the system change? Usually it did not, and the section is two lines. When it did, it is the first thing to read.

What it does not see

The map is built from imports and declarations as they are written. Dependency injection, reflection and dynamic calls are invisible to it, so a rule can be broken at runtime without the check noticing. Rules are about which component may depend on which; they do not express things like "every handler must be idempotent". Those still belong in your conventions and in review.

Where to start

Write the one rule your team already repeats in review, usually some version of "the domain does not touch the database", and set it to report first. A week of pull requests shows whether the check is right before it starts sending work back. The architecture map page shows the views, and we described the other gates a run passes in four checks an AI run should pass before you see it.

Back to all articles

Test it on a task from your own backlog

Send us one real task. We run CodeLynq on your repository and you review the pull request. Free, within 5 working days.