Como obriguei a IA a seguir o padrão do projeto com ESLint
A IA escreve código muito rápido. O problema é que ela escreve rápido do jeito dela.
Em uma sessão ela cria src/pages/UserProfile. Na outra, src/pages/user-profile. Em um momento importa o repositório direto no controller. Em outro inventa uma pasta nova no meio da estrutura. O código até funciona, mas o projeto vai virando uma colcha de retalhos.
E não adianta apenas ter um AGENTS.md em cada pasta. Documentação é ótima para explicar o padrão, mas a IA lê quando lembra, interpreta quando quer e, em sessões longas, perde o contexto. Regra que não quebra o build é sugestão.
Para mim, a virada foi parar de pedir e começar a forçar.
O padrão precisa ser executável
Se o projeto já usa ESLint, ele já tem o lugar certo para isso. O problema é que as regras tradicionais validam estilo e erros de código, mas não a estrutura do projeto nem a direção das dependências.
A diferença entre documentar e executar é grande:
AGENTS.md | ESLint |
|---|---|
| Explica a intenção | Executa a intenção |
| Depende do contexto da sessão | Funciona igual em toda execução |
| Pode ser ignorado | Quebra o lint e o CI |
| Ajuda a IA a entender | Obriga a IA a corrigir |
Foi por isso que criei o eslint-plugin-ai-rules.
Ele é um plugin de flat config para projetos com IA que valida:
- a hierarquia de pastas;
- o casing de pastas e arquivos;
- prefixos, sufixos e padrões de nome;
- quais camadas podem importar quais outras;
- a organização de imports.
Na prática, a IA pode até tentar sair do padrão. O lint aponta, o CI falha e ela é obrigada a corrigir antes de entregar.
A configuração é curta
O plugin exige ESLint 9.22+ ou 10, flat config e Node.js 20+. Não há suporte a .eslintrc.
npm i -D eslint-plugin-ai-rules eslint typescript typescript-eslint eslint-import-resolver-typescript
import { defineConfig } from 'eslint/config'
import aiRules from 'eslint-plugin-ai-rules'
import tseslint from 'typescript-eslint'
export default defineConfig([
...tseslint.configs.recommended,
...aiRules.configs.projects([
{ rootFolder: '.', preset: 'frontend' },
]),
])
Com isso o projeto passa a validar a estrutura inteira. Existem presets para frontend, backend e other, e você pode declarar sua própria estrutura quando o preset não servir.
Em monorepo, basta uma entrada por projeto:
...aiRules.configs.projects([
{ rootFolder: 'apps/web', preset: 'frontend' },
{ rootFolder: 'apps/api', preset: 'backend' },
]),
Raízes aninhadas também funcionam. O plugin usa a configuração mais específica para cada arquivo.
Se você usar apenas aiRules.configs.recommended, sem projects(), a regra ai-rules/project-setup avisa que a validação de estrutura ainda não foi configurada.
O que o plugin valida
Na prática, a validação acontece em quatro frentes:
- Estrutura — pastas permitidas, hierarquia, arquivos obrigatórios e pastas dinâmicas como
[feature]. - Nomes —
PascalCase,camelCase,kebab-case,snake_case, prefixos, sufixos e regex. - Arquitetura — quais camadas podem importar quais outras, inclusive com escopo por página ou módulo.
- Imports — ordenação, quebra de linha e duplicidade.
O erro aparece antes do code review
Quando a IA cria uma pasta fora do padrão, o diagnóstico é direto:
src/pages/MyFeature/Dashboard.tsx
error Folder "MyFeature" inside "src/pages" must match kebab-case. ai-rules/project-structure
Quando tenta furar a arquitetura:
src/http/controllers/ListProducts.ts
error Dependencies to elements of type "repository" are not allowed in elements of type "controller". boundaries/dependencies
O melhor é que a IA costuma corrigir sozinha. Ela roda o lint, lê a mensagem, ajusta o caminho ou o import e tenta de novo. Sem isso, esse mesmo erro chegaria no code review, ou pior, no repositório.
As relações de arquitetura são configuráveis:
...aiRules.configs.projects([
{
rootFolder: 'apps/api',
preset: 'backend',
relations: {
rules: [
{ from: 'controller', allow: ['useCase', 'helper'] },
{ from: 'useCase', allow: ['repository', 'service'] },
],
},
},
])
E a estrutura também aceita regras específicas, como arquivos com sufixo obrigatório:
structure: {
folders: {
src: {
folders: {
'use-case': {
folders: {
'[feature]': {
files: [
{
case: 'PascalCase',
suffix: 'UseCase',
extensions: ['.ts'],
},
],
},
},
},
},
},
},
}
Presets são ponto de partida, não camisa de força
Eu gosto das regras que o plugin aplica por padrão: imports ordenados, sem ponto e vírgula, interfaces com prefixo I, tipos com prefixo T.
Mas muita gente não gosta. E impor gosto pessoal é uma péssima forma de fazer adoção de ferramenta.
Por isso, na versão 0.4.0, o plugin passou a aceitar rules por projeto e a garantir que qualquer regra possa ser desligada, ativada ou ter a severidade alterada usando o objeto rules padrão do ESLint. Nada de lista proprietária de regras desativadas.
Dá para configurar por projeto:
...aiRules.configs.projects([
{
rootFolder: '.',
preset: 'frontend',
rules: {
'ai-rules/sort-imports': 'off',
'ai-rules/project-structure': 'warn',
'@stylistic/semi': ['warn', 'always'],
},
},
])
Ou sobrescrever depois do plugin:
export default defineConfig([
...tseslint.configs.recommended,
...aiRules.configs.projects([...]),
{
rules: {
'ai-rules/no-multiline-imports': 'off',
},
},
])
A precedência é:
- regras do
recommended; - regras automáticas de estrutura e relations;
- regras do preset;
rulesdo projeto;- qualquer config do ESLint colocada depois.
Isso vale para as regras do próprio plugin e também para as regras agrupadas de @stylistic, eslint-plugin-import-x, eslint-plugin-boundaries e @typescript-eslint.
Alguns exemplos do que dá para desligar ou ajustar:
| Regra | Padrão | Para que serve |
|---|---|---|
ai-rules/sort-imports | error | Ordena imports |
ai-rules/no-multiline-imports | error | Mantém imports em uma linha |
ai-rules/filename-pascal-case | error | Exige PascalCase em nomes de arquivo |
ai-rules/project-structure | error | Valida a estrutura declarada |
ai-rules/project-setup | warn | Avisa quando a estrutura não foi configurada |
@stylistic/semi | ['error', 'never'] | Proíbe ponto e vírgula |
@typescript-eslint/naming-convention | prefixos I e T | Padroniza interfaces e tipos |
projects() já desliga filename-pascal-case quando project-structure está ativo, porque o nome do arquivo passa a ser validado junto com a estrutura.
Adoção gradual
O plugin não precisa entrar em um projeto legado já como erro em tudo.
Se a estrutura atual ainda não segue o padrão, comece avisando:
rules: {
'ai-rules/project-structure': 'warn',
}
Também é possível ignorar árvores legadas:
structure: {
ignoredPaths: ['legacy/**'],
}
Ou desligar a validação de estrutura quando o projeto não se encaixa nos presets:
...aiRules.configs.projects([
{ rootFolder: '.', preset: 'other' },
])
Se você só quer silenciar o aviso de configuração temporariamente:
AI_RULES_SILENT=1 npx eslint .
A ideia é usar o ESLint como contrato, mas começar com o contrato que o time consegue cumprir hoje.
O que muda no dia a dia
A primeira mudança é de expectativa. O padrão deixa de ser uma recomendação no AGENTS.md e passa a ser uma condição para o código existir.
Na prática, isso traz:
- feedback imediato para a IA, antes de qualquer revisão humana;
- CI bloqueando desvios de estrutura e arquitetura;
- pastas e nomes consistentes entre sessões diferentes;
- menos tempo revisando detalhes que já deveriam ser automáticos;
- camadas respeitadas, como controller sem acessar repository direto;
- suporte a monorepo e a múltiplos projetos no mesmo repositório;
- autofix para parte das regras de import;
- liberdade para desligar ou alterar regras que não fazem sentido para o time.
No meu fluxo, praticamente não escrevo código manualmente. A IA mantém uma estrutura profissional porque não consegue passar pelo lint sem seguir o que foi definido. Quando ela tenta inventar uma pasta, o ESLint mostra o caminho de volta.
O que isso não resolve
O plugin não substitui testes, code review ou bom senso. Ele também não adivinha a arquitetura ideal do seu projeto: você precisa declarar a estrutura que quer manter.
A diferença é que, depois de declarada, ela deixa de depender da boa vontade do agente.
Conclusão
AGENTS.md explica o padrão. O ESLint obriga.
Para projetos que já usam ESLint e são desenvolvidos com IA, o eslint-plugin-ai-rules transforma combinado em contrato executável. Se o código sair do padrão, o lint acusa como erro. Se o lint falhar, o CI não passa. E se o CI não passa, a IA corrige.
Na versão 0.4.0, o plugin deixou de ser um conjunto fixo de opiniões e passou a ser um ponto de partida configurável. Você escolhe o que vale a pena manter, o que vira aviso e o que não faz sentido para o seu projeto.
É uma camada simples, mas que muda a qualidade da entrega.
- npm: https://www.npmjs.com/package/eslint-plugin-ai-rules
- GitHub: https://github.com/lvsouza/eslint-plugin-ai-rules
- Licença: MIT