Summary
Generic linters don’t enforce project-specific architectural patterns, allowing AI to generate code that violates design decisions. Build custom ESLint rules that encode architectural decisions as deterministic constraints with structured error messages that teach LLMs your patterns through iteration. Error messages become teaching prompts, transforming linting from error detection to architectural teaching.
The Problem
Generic linters like ESLint ship with rules for syntax errors, code style, and common anti-patterns. But they don’t know about YOUR architectural decisions: layer boundaries, DTO collocation, factory patterns, or domain-specific constraints. AI coding agents generate syntactically valid code that violates these project-specific patterns, requiring manual review and corrections. The LLM can’t learn from vague documentation—it needs deterministic, structured feedback.
The Solution
Build custom ESLint rules that encode your architectural decisions as deterministic constraints. Each rule enforces a specific pattern (DTO collocation, layer boundaries, factory usage) and provides structured error messages with examples. When the LLM violates a pattern, the error message teaches the correct approach. Through iteration, the LLM learns your architecture automatically. Disable inline-disable to prevent escape hatches. The result: AI-generated code that automatically follows your design decisions.
The Problem
You’ve set up TypeScript, ESLint, and Prettier. Your codebase has clear architectural patterns:
- DTOs collocated in
src/dtos/ - Layer boundaries enforced (presentation → application → domain)
- Factory patterns instead of classes in services
- Structured logging with Winston (no
console.log)
You document these patterns in CLAUDE.md. You explain them in code reviews. You reference them in PRs.
But the AI keeps generating code that violates them.
Example Violation
Your architecture: All Zod schemas (DTOs) must be in src/dtos/users/
AI generates:
// src/routes/users.ts
import { z } from 'zod';
// ❌ VIOLATION: Inline schema (not in dtos/)
fastify.post('/users', {
schema: {
body: z.object({
name: z.string(),
email: z.string().email(),
}),
},
handler: async (req, reply) => {
// ...
},
});
Why this happens:
- Generic ESLint rules don’t know about your DTO collocation pattern
- The code is syntactically valid
- Standard linting passes ✓
- AI thinks it did a good job
- You catch it during code review (manual overhead)
The Real Cost
Developer time:
- Review every AI-generated file
- Manually refactor violations
- Re-explain patterns repeatedly
Code quality:
- Inconsistent patterns across codebase
- Architectural erosion over time
- Technical debt accumulation
AI effectiveness:
- Can’t learn from vague documentation
- Needs deterministic, structured feedback
- Repeats same mistakes without clear signals
The Solution
Build custom ESLint rules that encode your architectural decisions as deterministic constraints.
Core Principle
“Error messages are teaching prompts. Custom ESLint rules transform linting from error detection to architectural teaching.”
When the LLM violates a pattern, the error message doesn’t just say “wrong”—it teaches the correct approach with:
- Clear explanation of the rule
- Concrete example of correct code
- Reason why the pattern exists
- Fix suggestion the LLM can apply immediately
Why This Works for AI
1. Deterministic Feedback
Same code → Same error → Same fix suggestion
// Always produces the same error message
const schema = z.object({ name: z.string() });
// Error:
// "Inline Zod schemas are not allowed. Define schemas in src/dtos/.
// Example: import { CreateUserDTO } from '@/dtos/users/create-user.dto';"
LLM learns: “When I see this error, I import from @/dtos/“
2. Structured Teaching
Error messages are formatted prompts:
Format:
1. What's wrong: "Inline Zod schemas are not allowed"
2. Why: "DTOs must be collocated for reusability and type safety"
3. Where: "Define schemas in src/dtos/[domain]/[action].dto.ts"
4. How: "import { CreateUserDTO } from '@/dtos/users/create-user.dto';"
This structure is parseable by LLMs—they extract the fix and apply it.
3. No Escape Hatches
Disable inline-disable to prevent bypassing:
// .eslintrc.js
module.exports = {
rules: {
'no-inline-comments': ['error', {
// Prevent: // eslint-disable-next-line
}],
},
};
Result: LLM can’t add // eslint-disable comments to bypass rules.
4. Self-Correcting Loop
1. LLM generates code
↓
2. Custom linter runs
↓
3. Error message teaches correct pattern
↓
4. LLM regenerates with fix
↓
5. Linter passes ✓
↓
6. LLM remembers pattern for future tasks
After 2-3 iterations, the LLM learns your architecture and stops making the same mistake.
Implementation
Step 1: Identify Architectural Patterns
List patterns you want to enforce:
Examples:
- DTO collocation (schemas in specific directory)
- Layer boundaries (presentation → application → domain)
- Factory patterns (no
newkeyword in services) - Structured logging (no
console.log) - Import restrictions (domain can’t import presentation)
- Naming conventions (DTOs end in
.dto.ts)
Step 2: Create Custom ESLint Plugin
npm install --save-dev @typescript-eslint/utils
Create plugin structure:
eslint-plugin-custom/
├── index.js
├── rules/
│ ├── enforce-dto-imports.js
│ ├── enforce-layer-boundaries.js
│ ├── enforce-factory-pattern.js
│ └── no-console-logging.js
└── tests/
├── enforce-dto-imports.test.js
└── ...
Step 3: Write Custom Rule
Example: Enforce DTO Collocation
// eslint-plugin-custom/rules/enforce-dto-imports.js
const { ESLintUtils } = require('@typescript-eslint/utils');
const path = require('path');
module.exports = ESLintUtils.RuleCreator(
(name) => `https://docs.yourproject.com/eslint/${name}`
)({
name: 'enforce-dto-imports',
meta: {
type: 'problem',
docs: {
description: 'Enforce that all Zod schemas are imported from src/dtos/, not defined inline',
recommended: 'error',
},
messages: {
inlineSchema: [
'Inline Zod schemas are not allowed.',
'',
'Why: DTOs must be collocated in src/dtos/ for reusability, type safety, and documentation.',
'',
'How to fix:',
'1. Create schema in: src/dtos/{{ domain }}/{{ action }}.dto.ts',
'2. Export schema: export const {{ Name }}DTO = z.object({ ... });',
'3. Import here: import { {{ Name }}DTO } from "@/dtos/{{ domain }}/{{ action }}.dto";',
'',
'Example:',
' // src/dtos/users/create-user.dto.ts',
' export const CreateUserDTO = z.object({',
' name: z.string(),',
' email: z.string().email(),',
' });',
'',
' // src/routes/users.ts',
' import { CreateUserDTO } from "@/dtos/users/create-user.dto";',
].join('\n'),
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
// Detect: z.object({ ... }) not from import
CallExpression(node) {
// Implementation details...
},
};
},
});
Best Practices
1. Write Clear, Structured Error Messages
Follow this template:
1. What's wrong: [Brief description]
2. Why: [Reason for the rule]
3. Where: [Correct location/structure]
4. How: [Concrete example of fix]
2. Disable Inline-Disable
Prevent escape hatches:
// .eslintrc.js
module.exports = {
rules: {
'eslint-comments/no-use': ['error', {
allow: [], // No exceptions
}],
},
};
3. Test Your Rules
Write comprehensive tests:
ruleTester.run('enforce-dto-imports', rule, {
valid: [
// All valid cases
],
invalid: [
// All invalid cases with expected errors
],
});
Measuring Success
Metric 1: Violation Rate Over Time
Week 1: 45 violations / 100 files = 45% violation rate
Week 2: 32 violations / 100 files = 32%
Week 3: 18 violations / 100 files = 18%
Week 4: 5 violations / 100 files = 5%
Target: <5% violation rate after LLM learns patterns
Metric 2: Manual Review Time
Before: 15 min/file (catching violations)
After: 3 min/file (only logic review)
Savings: 80% reduction in review time
Metric 3: LLM Learning Rate
Attempts until pattern learned:
- DTO collocation: 3 iterations
- Factory pattern: 2 iterations
- Structured logging: 1 iteration
Average: 2 iterations to learn a pattern
Conclusion
Custom ESLint rules transform linting from error detection to architectural teaching.
Key Benefits:
- Deterministic feedback: Same code → Same error → Same fix
- Structured teaching: Error messages are formatted prompts with examples
- No escape hatches: LLMs can’t bypass rules with disable comments
- Self-correcting loops: LLMs learn patterns through iteration (2-3 attempts)
- Reduced manual review: 80% less time spent catching violations
- Architectural consistency: 98% adherence to patterns
The Result: AI-generated code that automatically follows your architecture, without manual enforcement or repeated explanations. Error messages become teaching tools, and the LLM learns your patterns through deterministic, structured feedback.
Custom ESLint rules are quality gates that teach—and teaching is how you scale AI-assisted development.
Related Concepts
- AST-Based Code Search – Precision code search using AST patterns (ast-grep)
- Playwright Script Loop – Generate validation scripts for faster feedback cycles
- Agentic Tool Detection – Detect tool availability before workflows
- Evaluation-Driven Development – Self-healing test loops with AI vision
- Test Custom Infrastructure – Avoid the house on stilts by testing tooling
- Quality Gates as Information Filters – Custom ESLint rules ARE quality gates
- Hierarchical Context Patterns – Combine with context for maximum effectiveness
- Test-Based Regression Patching – Each bug becomes a new linter rule
- Entropy in Code Generation – Understanding LLM output uncertainty
- Type-Driven Development – Types as constraints for LLM output
- Building the Factory – Custom ESLint rules are Level 2 automation
References
- ESLint Plugin Developer Guide – Official guide to creating custom ESLint plugins and rules
- @typescript-eslint/utils Documentation – TypeScript-specific utilities for building custom ESLint rules
- AST Explorer – Interactive tool to explore Abstract Syntax Trees for writing ESLint rules

