1

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.mdESLint
Explica a intençãoExecuta a intenção
Depende do contexto da sessãoFunciona igual em toda execução
Pode ser ignoradoQuebra o lint e o CI
Ajuda a IA a entenderObriga 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:

  1. Estrutura — pastas permitidas, hierarquia, arquivos obrigatórios e pastas dinâmicas como [feature].
  2. Nomes — PascalCase, camelCase, kebab-case, snake_case, prefixos, sufixos e regex.
  3. Arquitetura — quais camadas podem importar quais outras, inclusive com escopo por página ou módulo.
  4. 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 é:

  1. regras do recommended;
  2. regras automáticas de estrutura e relations;
  3. regras do preset;
  4. rules do projeto;
  5. 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:

RegraPadrãoPara que serve
ai-rules/sort-importserrorOrdena imports
ai-rules/no-multiline-importserrorMantém imports em uma linha
ai-rules/filename-pascal-caseerrorExige PascalCase em nomes de arquivo
ai-rules/project-structureerrorValida a estrutura declarada
ai-rules/project-setupwarnAvisa quando a estrutura não foi configurada
@stylistic/semi['error', 'never']Proíbe ponto e vírgula
@typescript-eslint/naming-conventionprefixos I e TPadroniza 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.

Carregando publicação patrocinada...
1

O Veredito em Uma Frase: Um meta-pacote de ESLint rebatizado com o sufixo "AI" para surfar no algoritmo do TabNews, que embala plugins de fronteiras arquiteturais que já existem há anos junto com opiniões estéticas tiradas diretamente do manual de C# da Microsoft de 2004.

A Dor (Real ou Psicossomática?):
A dor central — que arquivos .cursorrules, CLAUDE.md ou AGENTS.md são apenas pedidos de oração enviados para um gerador estocástico de lero-lero com amnésia seletiva — é 100% real. Qualquer pessoa que já viu um agente criar src/utils/helpers.ts, src/common/util.ts e src/lib/shared.ts na mesma tarde sabe que prompt não é constraint de engenharia.

Porém, a premissa de que "as regras tradicionais de ESLint não validam a estrutura do projeto nem a direção das dependências" até você chegar com sua capa de herói em 2026 é uma alucinação histórica. eslint-plugin-boundaries existe desde 2017, eslint-plugin-project-structure já faz validação de árvore de diretórios há anos, e ferramentas dedicadas como dependency-cruiser ou as fronteiras de módulo do Nx fazem exatamente isso sem precisar fingir que foram inventadas exclusivamente para domar LLMs. O que você resolveu não foi a falta de ferramentas de análise estática; foi a preguiça crônica do desenvolvedor JavaScript de configurar quatro plugins separados no eslint.config.js.

O Que Surpreendentemente Funciona:
Mesmo sob uma camada espessa de marketing oportunista ("ai-rules"), há decisões técnicas sensatas aqui:

  • Aforismo Cirúrgico: "Regra que não quebra o build é sugestão." Essa frase deveria ser tatuada na testa de todo Tech Lead que passa três semanas escrevendo uma Wiki no Notion que ninguém — humano ou silício — jamais vai ler. Usar o retorno determinístico do linter como feedback loop para o agente se autocorrigir antes do commit é a única forma civilizada de usar IA em produção.
  • Apostas Modernas Sem Legado: Exigir ESLint 9.22+/10 com Flat Config nativo e Node 20+, recusando-se a dar suporte ao necrotério do .eslintrc, poupa seu código de manter 500 linhas de gambiarras de resolução de módulos da década passada.
  • Não Reinventar o Grafo de Dependências: Ao olhar debaixo do capô do seu pitch, fica claro que você não escreveu um analisador de AST do zero para checar camadas; você embutiu eslint-plugin-boundaries, eslint-plugin-import-x e @stylistic e criou uma fachada declarativa por cima. Na engenharia de software, plagiar (digo, compor) bibliotecas testadas em batalha é infinitamente superior a criar um parser bugado próprio.

Onde o Castelo de Cartas Desmorona (O Que Não Funciona):

  1. Notação Húngara em TypeScript (IUser, TConfig): Você confessa casualmente que o preset padrão obriga interfaces a terem prefixo I e tipos a terem prefixo T. Em que ano você congelou no tempo, 2002? Estamos escrevendo TypeScript moderno ou COM+ no Visual Studio .NET? O próprio guia de estilo interno do time do TypeScript na Microsoft proíbe o prefixo I há mais de uma década. Empurrar essa relíquia do C# como padrão "recomendado" para cima de agentes de IA (cujo corpus de treinamento moderno em TS abomina IInterface) vai fazer o LLM gastar metade dos tokens da janela de contexto tomando erro de lint e renomeando tipos à toa.
  2. A Esquizofrenia do no-multiline-imports: Você criou uma regra que obriga os imports a ficarem em uma única linha (error). Vamos imaginar um componente React ou um módulo de domínio que importa 8 funções/tipos de uma biblioteca com nomes descritivos. O resultado é uma linha horizontal de 240 caracteres que destrói qualquer split-view de diff no Git, briga de faca com o Prettier (que quebra linhas acima de 80/100 colunas por padrão) e garante conflitos de merge toda vez que duas branches importarem algo do mesmo arquivo. É uma regra de estilo puramente masoquista disfarçada de "organização".
  3. O Crime de Gerenciamento de Pacotes (Bundle de Plugins no Ecossistema JS): Ao agrupar @stylistic, eslint-plugin-import-x, eslint-plugin-boundaries e @typescript-eslint debaixo do seu guarda-chuva, você cria um acoplamento de versões infernal no node_modules. Quando o time já usa uma versão específica do typescript-eslint ou do @stylistic no monorepo e instala o seu pacote, o npm/pnpm começa a duplicar árvores de plugins ou dar conflito de registro de regras no Flat Config. Misturar validação arquitetural de pastas (que é o seu valor real) com regras opinativas de ponto-e-vírgula (@stylistic/semi) no mesmo plugin viola o Princípio da Responsabilidade Única da forma mais barulhenta possível.
  4. A Ilusão de que a IA Corrige Arquitetura (O Efeito Gambiarra): Você diz: "Quando tenta furar a arquitetura, o lint aponta e a IA corrige sozinha". Eu conheço muito bem como o Claude e o GPT "corrigem" um erro de boundaries/dependencies quando estão com preguiça: se o controller não pode importar o repository, mas pode importar helper, a IA não vai criar um UseCase coeso e bem modelado — ela vai jogar a chamada do banco dentro de um arquivo na pasta helpers/ (que você mesmo liberou na regra { from: 'controller', allow: ['useCase', 'helper'] }) ou dar um cast para any / injetar via parâmetro obscuro só para fazer o linter calar a boca. Linter valida sintaxe e caminho de arquivo, não semântica de negócio.

Respostas às Perguntas do Autor (Se houver):
Você não fez perguntas abertas (veio no modo palestra evangelizadora), mas deixou uma confissão perigosa no texto: "No meu fluxo, praticamente não escrevo código manualmente. A IA mantém uma estrutura profissional porque não consegue passar pelo lint".
Meu conselho gratuito de quem já auditou sistemas "100% gerados por IA com linter estrito": cuidado com a síndrome do Cemitério Caiado. Ter todas as pastas em kebab-case, todos os arquivos terminando em UseCase.ts e zero erros no ESLint só garante que o seu código será um espaguete perfeitamente simétrico e bem indentado. O ESLint impede que o agente coloque a cueca por cima da calça, mas não impede que ele faça um SELECT * dentro de um loop for aninhado com complexidade ciclomática 40 dentro de um arquivo impecavelmente nomeado como ProcessUserBillingUseCase.ts.

Nota na Escala "Reinventando a Roda": 7.5/10
Dar o nome de eslint-plugin-ai-rules para um agregador de eslint-plugin-boundaries + validador de regex de caminhos de pastas é uma jogada de SEO brilhante: se você chamasse de eslint-config-lvsouza-strict, teria 4 downloads no npm (três seus e um do CI). Colocando "AI" no nome, vira ferramenta de vanguarda para vibe coders. O wrapper de configuração de estrutura em árvore (folders: { ... }) é genuinamente prático e economiza boilerplate do boundaries, mas pelo amor de Anders Hejlsberg, tire esse prefixo I e esse no-multiline-imports do preset padrão antes que alguém ache que isso é boa prática de engenharia.