The Architecture Rules a Linter Can't Check

mooreds1 pts0 comments

The Architecture Rules a Linter Can't Check | Marcos Defendi

← All posts

Architecture

The Architecture Rules a Linter Can't Check

Published in June 2026 · 11 minute read

Every large codebase has rules that no linter knows about. This package must not<br>import from that one. This business rule lives in exactly one place. The domain layer<br>never touches the database directly. These rules live in code review comments,<br>onboarding chats, and a few people's heads.

And they slip away quietly, one reasonable-looking PR at a time. Every single change<br>looks fine on its own. The problem only exists in the relationship between things — an<br>import that reaches somewhere it shouldn't, a rule that grew a second copy in a second<br>place, a value that quietly took on a second meaning — and nobody reviews relationships.<br>A reviewer sees one change, not the whole shape it's part of. I've<br>written<br>before about why code review can't close that gap on its own.

The usual answer to that is a fitness function: an automated check that guards an<br>architectural property, so the rule fails a build instead of failing a memory. It's a<br>good answer, and most teams stop well short of where it can actually reach. So the<br>question I kept circling is how far it goes — which of these agreements you can really<br>turn into a check, and which ones quietly resist it. That's what this post is about. The<br>examples below come from running real rules against real code; it starts with the kind a<br>machine obviously handles, and ends with the kind you'd assume it couldn't.

Part 1: the rule a linter could almost express

Cal.com<br>is a large TypeScript codebase, and to their credit, they wrote<br>their architecture rules down. Not in a wiki nobody reads, but in the<br>agents/rules/ files they feed to their AI coding agents. Two of them<br>matter here. The first,<br>architecture-circular-dependencies.md,<br>sets a strict order for which package can use which, and says, as rule 6, word for word:

No files in packages/app-store import from @calcom/features<br>or ../features/**

The second,<br>architecture-feature-boundaries.md,<br>adds: "All cross-feature dependencies must go through the feature's public API." Both<br>are good rules. app-store sits below features in their layers,<br>so reaching up into it is backwards. The only question left is whether the code still<br>follows them.

I turned rule 6 into a maat rule. Same boundary, written as code:

layer('@calcom/app-store')<br>.forbids(/@calcom\/features\/[^/]+\/(lib|repositories|services)\//)<br>.build()

And ran the check:

FINDINGS (16)<br>────────────<br>[layer-imports]: 16 findings

"@calcom/app-store" imports feature internals<br>(forbidden by the rule). One example:

↳ app-store/_utils/payments/<br>handlePaymentSuccess.ts<br>→ @calcom/features/bookings/lib/EventManager

Sixteen places where app-store reaches into the inner parts of<br>features, against their own written rule. One file on its own,<br>handlePaymentSuccess.ts, imports from twelve paths inside<br>features, across four different feature areas (bookings, webhooks,<br>platform-oauth-client, tasker).

Here's the part that makes Cal.com a good example and not a target: they don't just<br>write the rule down, they enforce it. The boundaries doc even says so:<br>"Domain boundaries are enforced automatically through linting." And it's true. Their<br>Biome config<br>has a rule for exactly this:

// biome.json, packages/app-store override<br>"noRestrictedImports": {<br>"level": "error",<br>"options": {<br>"patterns": [<br>{ "group": ["../features/**"],<br>"message": "app-store package should not<br>import from features ..." }

So how did sixteen imports get past a rule set to error? Look closely at<br>what it matches: ../features/**, the relative path. The imports<br>that slipped through use the package alias instead:<br>@calcom/features/bookings/lib/.... Same boundary, written a different way,<br>and the linter only knew about one way. Here's the telling part. Rule 6, as they<br>wrote it, names both forms (@calcom/features and<br>../features/**), and they clearly know how to block both, because in<br>their testing config they list exactly that pair. The Biome rule for<br>app-store just didn't carry over the full rule they had written.<br>It's a gap, not a choice.

That's the whole problem in a nutshell, and it isn't really about any one tool. A<br>text-matching check sees the string you typed; the boundary you actually care about is<br>the dependency underneath it. Resolve the import to where it truly points and the alias<br>and the relative path collapse into the same edge — but if you're matching strings, they<br>don't, and one form sails through. The rule was written down, even partly enforced, and<br>it still slipped through the gap between the two spellings.

This is the familiar end of the spectrum, though — the kind of rule you'd expect to be<br>checkable, and roughly half a linter already gets there. I start here precisely because<br>it's unremarkable: concrete, mechanical, and something anyone can verify for themselves.<br>It's also why this is the part I ran on Cal.com — it's their rule,...

rule features rules store architecture linter

Related Articles