My Projects With ESLint Stay Clean. The Rest Drift.
Publicado em 24 de mar. de 2026 · 5 min de leitura
No Chrome, toque em Mais → Ouvir esta página para ouvir o post
On this page 6 sections
Configurações de leitura
My Projects With ESLint Stay Clean. The Rest Drift.
Same AI agent. Same workflow. Same architectural spec. The projects where I configured ESLint to enforce architectural boundaries stay clean. The ones without an equivalent gate drift. I don’t notice until an audit surfaces dozens of violations.
I’ve been building both with Claude Code. The difference is whether the toolchain enforces the spec or merely documents it.
↑ Contents
ESLint isn’t just a style enforcer
When most developers think about ESLint, they think about the petty stuff: semicolons, spacing, unused variables. Style enforcement. The thing you configure once and forget.
My eslint.config.mjs does that. It also does something more important, via eslint-plugin-boundaries — a plugin that enforces architectural layer rules as lint errors.
Diplomat Architecture — layer boundary enforcement
// Diplomat Architecture — layer boundary enforcement
{
plugins: { boundaries },
settings: {
"boundaries/elements": [
{ type: "models",
pattern: "models",
mode: "folder" },
{ type: "wire",
pattern: "wire",
mode: "folder" },
{ type: "adapters",
pattern: "adapters",
mode: "folder" },
{ type: "logic",
pattern: "logic",
mode: "folder" },
{ type: "controllers",
pattern: "controllers",
mode: "folder" },
{ type: "diplomat",
pattern: "diplomat",
mode: "folder" },
],
},
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{ from: ["models"],
allow: ["models"] },
{ from: ["wire"],
allow: ["wire", "models"] },
{ from: ["adapters"],
allow: ["adapters", "models", "wire"] },
{ from: ["logic"],
allow: ["logic", "models", "components"] },
{ from: ["controllers"],
allow: ["controllers", "models", "logic", "components"] },
{ from: ["diplomat"],
allow: ["diplomat", "models", "wire", "adapters", "logic", "controllers", "components"] },
],
}],
"boundaries/no-unknown": "error",
"boundaries/no-unknown-files": "error",
},
}
Every import in the project is checked against this dependency graph. A logic function that tries to import from adapters fails the build. A models file that reaches into controllers fails the build. The architecture is enforced by the toolchain, not by documentation, code review, or the model’s memory.
Because this is ESLint, the violation is an error. The build breaks. The agent cannot move on until it fixes the import. There is no “I’ll clean this up later” — the toolchain won’t let it. Write a bad import, get an error, fix it before anything else happens.
↑ Contents
A Perfect Spec Is Not a Gate
Another project I’ve been building is a CLI tool with a Ports and Adapters architecture. It didn’t have an equivalent gate. It had something better, I thought: a thorough architectural spec. Six hundred lines covering type algebra, port design rules, workflow signatures, wire schema conventions, naming conventions, and a full design checklist. Every step from “name the domain events” to “system assembly.” It had a syntax linter. It had a test suite with hundreds of assertions.
What it didn’t have was a tool that checked whether the code actually followed the spec.
So I wrote one. A 130-line script that checks namespace dependency direction, flags forward declarations (which signal circular dependencies), enforces file size limits, and validates error handling boundaries. When I ran it for the first time:
✗ 39 architectural violation(s) found:
handler.clj:78 [NO DECLARE] (declare maybe-sync!)
handler.clj:1301 [FILE TOO LARGE] 1301 lines (limit: 200)
doctor/core.clj:4 [DOMAIN → ADAPTER] (:require [adapter.wire.cli :as wire])
import/core.clj:3 [DOMAIN → ADAPTER] (:require [adapter.wire.cli :as wire])
...
Thirty-nine violations. Every one real. Every one the exact kind of problem a spec would warn about: dependency direction, mutation discipline, file size, error handling boundaries.
And every one invisible to the test suite.
↑ Contents
Why tests aren’t enough
Tests verify behavior: given this input, produce this output. That’s what they’re designed to do, and they do it well.
Architectural violations don’t change behavior. That’s exactly the problem. The code still runs. Tests go green. A domain function that reaches into the adapter layer still returns the right value. A 1,300-line file compiles without complaint. There’s no signal that anything is wrong. You only find out when you try to refactor, or when you run the linter for the first time and discover the structure has been rotting underneath the passing tests.
This is why the violations accumulated silently. Each commit passed the tests. Each AI subagent completed its task, declared victory, and moved on. The agent read the spec, understood it abstractly, and then made each micro-decision locally, optimizing for “make this function work” instead of “does this comply with the spec?”
The 600-line spec said “domain functions compile with no adapter imports.” The design checklist said “no declare in any file.” Nobody checked if domain was reaching into adapter. Nobody checked if the handler file had grown past any reasonable size. The structural constraints were in the spec, but nothing mechanical enforced them.
↑ Contents
Agents Don’t Feel Architectural Pressure
In the pre-AI era, a human wrote every line. Humans feel architectural pressure. They notice when a file grows to 1,300 lines because they’re the ones scrolling through it. They pause before introducing a circular dependency because they’ve seen where that leads. Architectural discipline is partially tacit, transmitted through discomfort rather than rules.
AI agents don’t feel discomfort. Each subagent receives a task like “implement remote commands”, reads the spec, and optimizes for completing it. It doesn’t notice that the handler file is already 800 lines and shouldn’t receive another 200. It sees a forward-reference error and reaches for declare to unblock itself. Each decision is locally reasonable. The drift emerges from hundreds of locally-reasonable calls that collectively diverge from the intended architecture.
The linter replaces tacit pressure with a hard gate. The pre-commit hook throws. The agent cannot commit until the violation is fixed. It doesn’t learn architectural discipline — it is forced to comply.
In my ESLint-enforced projects, every violation is caught before it lands. In the project without the gate, violations accumulated silently across sessions until someone finally looked.
↑ Contents
Tooling configuration is architecture
I spent a few hours configuring eslint-plugin-boundaries at the start of the project. Defining element types. Writing the dependency graph. Testing that the rules fired correctly. It felt like setup.
Those hours paid back many times over. Every session since, the architecture has held. Not because the agent perfectly internalized the spec. Because the linter enforced it.
This is the shift I want to name: in the age of AI-assisted development, tooling configuration is architecture work. The linter is the gate that makes your architectural decisions durable. It converts a spec from guidance the agent can drift from into a constraint the toolchain enforces.
ESLint is exceptional at this. The combination of first-class plugin support, editor integration, and inline feedback makes architectural enforcement a configuration problem, not a tooling problem. You write a JSON config, and every import in every file is checked against your dependency graph in real time. No custom scripts. No separate build step. No JVM startup penalty.
If your stack has ESLint, configure it like an architect. If it doesn’t, build the equivalent and wire it into your workflow. The projects with the gate stay clean. The rest drift.