Skip to content

Architecture checks for large codebases

Linters check lines. maat checks the agreements your team made about the codebase.

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 finding a layer violation in Cal.com

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?

Why this exists

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 problems linters can't see

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.

New and existing codebases

Greenfield

Write the rules before the shortcuts settle in.

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.

  • Prevent accidental dependencies before they become normal.
  • Keep domain code independent from infrastructure and framework details.
  • Review architecture rules as code.

Brownfield

Start from what the codebase already taught you.

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.

  • Separate new violations from existing debt.
  • Track accepted exceptions in the same repository history as the code.
  • Turn repeated review comments into checks.

Read more about greenfield and brownfield workflows

Who benefits most

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.

Design choices

Boring on purpose

Same facts in, same findings out. No hidden state, no randomness, no network calls, no AI judgment anywhere in the check path.

History lives with the code

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.

Adoptable before the codebase is clean

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.

What happens when it runs

Collect factsCollectors read plain facts from the repository: which files import which, what lives in which layer, what changes together in git history.
Check rulesRules compare those facts against the agreements your team chose to encode. Same facts in, same findings out.
Report findingsEach finding gets a stable ID, so the same problem stays the same problem across commits and renames. Findings based on AI-extracted facts are flagged for human verification.
Update the ledgerDecisions — accepted for now, fixed, declared — are stored in a file committed with the repository, so they travel with the code they describe.

Configuration example

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:

bash
npm install -D @maat-tools/cli @maat-tools/core @maat-tools/collector-ts @maat-tools/coupling-rules
ts
import { 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(),
  ],
})

Agreements a machine can't check yet

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:

bash
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."

Fitness functions

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

FAQ

Is maat a linter?

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.

How is it different from dependency rules?

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.

How is it different from architecture unit tests?

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.

Does AI decide what's a violation?

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.

Can we write our own checks?

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.

Status

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.

Released under the Apache-2.0 License.GitHub