Architecture checks for large codebases
Write your team's architecture rules as code, check them on every run, and keep a committed history of every violation and every decision made about it.

See it in action
maat finding a real layer violation in Cal.com's codebase: config → findings → the file that breaks the rule.

maat is not a linter, a code grader, or an AI reviewer. Linters tells you a line breaks a style rule. SonarQube gives your code a score. maat answers a different question: is the codebase still keeping the promises your team made about it — and if not, since when?
Every team has rules that no linter knows about. The domain layer never talks to the database directly. These two modules must not know about each other. This policy is implemented in one place only. Those rules live in code review comments, onboarding chats, and a few people's heads — and they erode quietly, one reasonable-looking PR at a time.
Most teams living with a hard-to-change codebase share the same picture: the same kinds of bugs keep coming back, the time goes to firefighting, and nobody asks why — that's just how work feels. When someone finally reviews the architecture by hand, they find the reasons: rules everyone had silently agreed on, broken little by little over years, where every individual change looked fine. And the review usually fails anyway — not because it's wrong, but because it arrives without evidence: no way to show when each rule started slipping, how fast, or what it's costing.
maat is that review, automated, with the receipts built in. It asks the question a good tech lead carries in their head — on every commit, with a paper trail.
The hardest coupling isn't in import graphs. It's in the things that have to stay in agreement without saying so: the same business policy implemented in three slightly different copies, a data shape shared between modules that read it with incompatible assumptions, two functions that must change together but live far apart.
These problems are invisible to linters, type checkers, and unit tests, because every individual line is fine. Types compile. Tests pass. The damage accumulates silently — bugs that keep coming back, data nobody can explain, gaps that surface months later.
maat makes these problems detectable. Collectors read plain facts from your code and git history. When a fact needs reading rather than parsing — like noticing that two functions implement the same policy differently — AI-assisted enrichers can extract it. But AI never gets a vote: boring, repeatable rules decide what counts as a violation. AI for reading. Rules for guarantees.
Greenfield
New systems can encode package boundaries, layer rules, and purity constraints from the first commit. maat check fails the pull request before an accidental dependency becomes precedent.
Brownfield
The first run on a mature codebase will find things. That's expected, and none of it counts against anyone — maat separates new violations from existing debt, so you can adopt rules without first fixing years of history.
Read more about greenfield and brownfield workflows
Backend codebases with real business logic, layers, and module boundaries — the bigger and older, the more maat has to say. Frontend projects tend to have less business logic encoded in structure, so a linter or type-checker often covers the same ground. If your frontend has complex state machines, domain models, or cross-module contracts, maat can still help.
Same facts in, same findings out. No hidden state, no randomness, no network calls, no AI judgment anywhere in the check path.
Findings and decisions are recorded in a plain file (the ledger) committed with the repository. maat stores the decision; git history stores who made it. Context survives when people leave — it's about memory, not blame.
Existing violations can be accepted for a limited time or marked as fixed. Accepted exceptions expire and force a revisit — there is no permanent "ignore".
Official rules from the maat-tools/maat repository carry the repeatability guarantee. Third-party plugins are supported through the same public interfaces, but their behavior is the responsibility of the package author and the team that installs them.
Read more about the repeatability guarantee and the plugin system.
maat ships with a TypeScript collector and built-in rules for package and layer boundaries. Teams can add their own collectors, enrichers, and rules for codebase-specific problems.
The CLI is just the runner — collectors, enrichers, rules, insights, and ledger backends are separate packages you install per project based on what you need:
npm install -D @maat-tools/cli @maat-tools/core @maat-tools/collector-ts @maat-tools/coupling-rulesimport { defineConfig } from '@maat-tools/core'
import { layer, Pure } from '@maat-tools/coupling-rules'
export default defineConfig({
check: { strict: true },
collectors: [['@maat-tools/collector-ts', { tsConfigFilePath: './tsconfig.json' }]],
rules: [
// "Business logic stays free of databases, HTTP, and frameworks."
layer('@myapp/domain').is(Pure).build(),
// "Infrastructure may use the domain and shared contracts. Nothing else."
layer('@myapp/infra').allows('@myapp/domain', '@myapp/contracts').build(),
],
})Some agreements can't be verified by a collector or rule yet. Write them down anyway, so they're versioned and visible instead of tribal:
maat axiom declare \
--id "domain-purity" \
--scope "@myapp/domain" \
--claim "The domain layer has no infrastructure dependencies." \
--note "Keeps the domain testable without spinning up real I/O."If you've read Building Evolutionary Architectures (Ford, Parsons, Kua, Sadalage), maat rules are fitness functions: automated checks that measure how well a system adheres to its intended architectural characteristics. You don't need the book to use maat — but if you have that vocabulary, this is where maat sits.
Read more about how maat implements fitness functions
No. Linters check local syntax, style, or common code patterns — one file at a time. maat is for rules that need facts from more than one place in the repository, including its history.
Dependency rules are one use case. maat can express package and layer boundaries, but rules can run over any fact a collector provides — including facts from git history and AI-assisted reading of the code.
Architecture tests pass or fail at one point in time. maat adds memory: existing findings can be accepted for a limited time, fixed ones are marked resolved, and the exact same problem coming back is caught as a regression — not rediscovered as something new.
Never. AI-assisted enrichers can extract facts that need reading rather than parsing — like two functions implementing the same policy differently. But the rules that judge those facts are plain, repeatable code, and findings based on AI-extracted facts are flagged for human verification.
Yes. maat is built around plugins: collectors gather facts, rules evaluate them, and the same public interfaces the official packages use are available to yours.
maat is pre-1.0. The CLI can run checks, sync findings with the ledger, and move decisions through baseline and resolve flows.
The model is stable enough to inspect and experiment with, but package APIs can still change while the collector and rule interfaces settle.