Lint-as-architecture (standards = enforced rules)
Custom lint rules that enforce architectural decisions turn code review comments into machine-checked constraints - so architectural violations are caught at CI time, not weeks later in production.
- ·AI review agent runs as a first-pass reviewer on every PR before human review
- ·Lint rules enforce architectural standards (not just style) - the "Bug to Codify to Lint Rule" pipeline is active
- ·At least 3 architectural guardrail rules have been created from past bugs or incidents
- ·AI review agent findings are categorized by severity (info, warning, blocking)
- ·New lint rules are proposed automatically when recurring review comments are detected
Evidence
- ·CI configuration showing AI review agent as required check
- ·Lint rule change history showing rules created from incident post-mortems
- ·AI review agent output logs with severity categories
What It Is
Lint-as-architecture is the practice of encoding architectural decisions as custom lint rules that run in CI. Instead of communicating architecture through documentation (which people don't read) or code review comments (which are reactive and inconsistent), you express the decision as a machine-executable rule that fails the build when violated.
Consider these common architectural decisions that teams currently communicate through review comments:
- "Don't call the database directly from HTTP handlers - use the service layer"
- "All API responses must use our standard
Result<T, ApiError>type" - "The
authmodule must not import frombilling- they're separate bounded contexts" - "Don't use
fmt.Printlnin production code - use the structured logger" - "External HTTP calls must go through the HTTP client abstraction, not the standard library directly"
At L2, these rules exist as expectations in people's heads, enforced through review comments when a reviewer remembers to check. At L3 (Systematic), these rules are encoded in the linting configuration and enforced automatically by CI. The architectural constraint exists as code, not as institutional memory.
Tools that support custom rules: ESLint custom rules (JavaScript/TypeScript), Pylint plugins (Python), custom golangci-lint linters (Go), ArchUnit (Java/Kotlin), NetArchTest (.NET), Rubocop custom cops (Ruby), and many others. Most modern linting frameworks are designed to be extended with domain-specific rules.
The shift from L2 to L3 on this dimension is a mindset shift as much as a tooling shift: architecture is only real if it's enforced. A decision recorded in Confluence but not in CI is a suggestion, not a constraint.
Why It Matters
Encoding architecture as executable rules has compounding benefits:
- Prevents regression - A constraint that exists only in institutional memory degrades over time. New team members don't know it. Existing team members forget it during a crunch. A lint rule enforces the constraint forever.
- Shifts enforcement left - Architectural violations caught in CI (seconds after commit) are dramatically cheaper to fix than violations caught in code review (hours later) or in production (days or weeks later).
- Makes architecture visible - The lint configuration is a machine-readable record of the team's architectural decisions. New team members can read the rules to understand what constraints exist and why.
- Enables faster code generation - When AI agents generate code at L4-L5, they often violate architectural constraints that aren't in their context. Custom lint rules catch these violations automatically, making AI code generation safer.
- Reduces reviewer cognitive load - Reviewers who know that architectural constraints are machine-enforced can stop mentally checking for violations. This is real cognitive capacity recovered for higher-order review.
The progression to L4 (auto-merge) and L5 (autonomous agents) depends critically on trusting your quality gate. Custom lint rules are a core component of that trust: they let you say with confidence that a Green PR has not introduced known architectural violations.
Write the "why" as a comment in every custom lint rule. Six months from now, no one will remember why no-direct-db-from-handler was added. A single-sentence comment in the rule definition ("Added after incident-2024-11: N+1 query problem in HTTP handlers") makes the constraint legible to the future team.
Getting Started
- Identify your most repeated review comments - What architectural constraints do your reviewers enforce through comments? A quick analysis of your PR history will surface the top 5-10 repeated comment themes. These are the candidates for custom lint rules.
- Start with one rule that enforces a clear boundary - Pick a simple, unambiguous constraint: "no imports from X module in Y module" or "no calls to this deprecated function." Write a custom lint rule for it. Getting one rule working teaches you the framework and demonstrates the value.
- Choose the right tool for your language - For JavaScript/TypeScript, ESLint's custom rules API is well-documented. For Java/Kotlin, ArchUnit provides a fluent API for testing architectural constraints. For Python, Pylint plugins or custom AST visitors work well. For Go, golangci-lint's plugin system supports custom linters.
- Test the lint rule before deploying - Write unit tests for the custom lint rule itself: cases that should fail, cases that should pass, edge cases. Lint rules with false positives will be disabled by frustrated developers.
- Document the architectural decision alongside the rule - The lint rule codifies the decision, but the rule file should reference the decision's rationale. An
ARCHITECTURE.mdor inline comment explaining "why this rule exists" is essential for future team members. - Add new rules progressively - Don't try to encode all architectural decisions as lint rules at once. Add one per sprint, starting with the highest-impact (most frequently violated or most serious when violated) constraints. The process of writing lint rules also surfaces which architectural decisions are actually clear vs. ambiguous.
Common Pitfalls
Writing lint rules that are too broad. A rule like "no direct database calls" that flags legitimate uses (in repository classes, for example) will generate false positives and be disabled. Rules need to distinguish between the allowed pattern and the forbidden pattern precisely. Spend time defining the exact pattern to flag before writing the rule.
Not maintaining custom rules. Custom lint rules break when the code they're analyzing changes structure. A rule that checks for a pattern that gets refactored away will either stop firing (false negatives) or start firing on unrelated code (false positives). Custom rules require maintenance. Assign ownership.
Using lint rules for things that belong in type systems. Some architectural constraints can be expressed in the type system (making illegal states unrepresentable) rather than in lint rules. A PaymentAmount type that wraps an integer can prevent "use raw integers for amounts" violations without a lint rule. Prefer type-system enforcement over lint-rule enforcement when possible - it's more robust.
Skipping the "why" - Lint rules without rationale are deleted by future developers who don't understand the constraint. "Why can't I import billing from auth?" If the rule doesn't explain, the developer disables it. Document the decision alongside the rule.
How Different Roles See It
Bob's team has had three incidents in the past six months caused by direct database access in HTTP handlers - each resulted in N+1 queries under load that crashed the service. After each incident, they added a note to the architecture documentation. The notes are there, but developers keep making the same mistake because no one reads the architecture docs before writing code.
What Bob should do: Bob should commission Victor (or a senior engineer with taste for tooling) to write a custom ESLint (or language-appropriate) rule: "no direct database access in HTTP handler files." The rule takes half a day to write and test. Once deployed, it will catch every future violation at CI time, before it reaches production. Bob should present this to his team as a category of incident that has been permanently eliminated. He should also establish a process: after every incident, ask "could a lint rule have prevented this?" If yes, write the rule. This is the Bug → Codify → Lint Rule process that makes quality gates progressively stronger.
Sarah is tracking post-merge bugs (bugs that were introduced in a PR that passed review). She's noticed that many of them fall into patterns: the same architectural violations appearing repeatedly across different PRs and different developers. Her team is fixing the same class of bugs every quarter.
What Sarah should do: Sarah should frame lint-as-architecture as a recurring bug elimination strategy. She can calculate the cost of the repeated architectural violations (incident time, on-call response, remediation) and compare it to the one-time cost of writing a lint rule (half a day of engineering time). The ROI for each rule is the expected incident cost eliminated, which is usually immediately obvious. Sarah should ask engineering leads to track "classes of bugs eliminated by lint rules" as a quality metric - a growing number indicates the team is progressively hardening its quality gate.
Victor keeps writing the same review comment: "Don't call the database from the HTTP handler layer." He's written it on 12 PRs in the past 3 months. He knows exactly what the bad pattern looks like and what the correct pattern is. He's confident he can write a lint rule for it.
What Victor should do: Victor should write the lint rule this sprint. It's a direct conversion of something he's doing manually (reviewing code for a pattern) into something automated (a lint rule that checks for the pattern). He should document the rationale in the rule, add unit tests for it, and submit it as a PR to the linter configuration repository. Once merged, Victor is no longer the enforcer of this constraint - CI is. He should count how many PRs over the next three months the lint rule blocks. That number is hours of review time recovered (his own and others'). Victor should make this visible to Bob and Sarah as evidence that lint-as-architecture is paying off.
Further Reading
From the Field
Recent releases, projects, and discussions relevant to this maturity level.
Where does your team actually sit on this?
This guide describes one level of one area. Run the assessment to place your team across all 16 areas, see which gates you have passed, and get a report you can take to your stakeholders.