Pular para o conteúdo

Escrevendo Regras

Regras são funções TypeScript que verificam seu codebase quanto à conformidade com ADRs. Elas ficam em arquivos .rules.ts complementares ao lado dos arquivos markdown dos ADRs e são executadas quando você roda archgate check.

.archgate/adrs/
ARCH-001-command-structure.md # The decision
ARCH-001-command-structure.rules.ts # The automated checks

Todo arquivo de regras exporta por padrão um objeto simples tipado com satisfies RuleSet. Cada chave no objeto rules se torna um ID de regra, e cada regra tem uma description e uma função async check que recebe um objeto de contexto.

/// <reference path="../rules.d.ts" />
export default {
rules: {
"my-rule-id": {
description: "What this rule checks",
async check(ctx) {
// Your check logic here
},
},
},
} satisfies RuleSet;

Um único arquivo de regras pode definir múltiplas regras:

/// <reference path="../rules.d.ts" />
export default {
rules: {
"first-rule": {
description: "Checks one thing",
async check(ctx) {
// ...
},
},
"second-rule": {
description: "Checks another thing",
async check(ctx) {
// ...
},
},
},
} satisfies RuleSet;

O objeto ctx passado para toda função check fornece capacidades de leitura de arquivos, busca e relatório. Aqui está uma referência detalhada com exemplos.

Um array de caminhos de arquivo que correspondem ao glob files do frontmatter do ADR. Se o ADR não tiver o campo files, isso inclui todos os arquivos do projeto.

for (const file of ctx.scopedFiles) {
const content = await ctx.readFile(file);
// Check content...
}

Use ctx.scopedFiles quando sua regra deve se aplicar apenas aos arquivos que o ADR governa. Por exemplo, uma regra de estrutura de comandos com escopo src/commands/**/*.ts receberá apenas arquivos de comandos.

Um array de caminhos de arquivo que diferem da branch base, incluindo alterações não commitadas da árvore de trabalho (arquivos staged, não staged e não rastreados que não sejam ignorados). Auto-detectado por padrão, ou preenchido via --staged / --base <ref>. Útil para verificação incremental e regras de dependência entre arquivos.

// Verificação incremental -- validar apenas arquivos alterados
const filesToCheck = ctx.scopedFiles.filter((f) =>
ctx.changedFiles.includes(f)
);
// Dependência entre arquivos -- se o arquivo A mudou, o arquivo B também deve mudar
if (ctx.changedFiles.includes("config/database.yml")) {
if (!ctx.changedFiles.includes("deploy/manifest.yml")) {
ctx.report.violation({
message: "config changed but manifest was not bumped",
file: "config/database.yml",
});
}
}

Lê o conteúdo de um arquivo como string. O caminho é relativo à raiz do projeto.

const content = await ctx.readFile("src/cli.ts");

Lê e faz o parse de um arquivo JSON. Retorna unknown — faça cast para o formato esperado.

const pkg = (await ctx.readJSON("package.json")) as {
dependencies?: Record<string, string>;
};

Busca em um único arquivo com uma expressão regular. Retorna um array de objetos GrepMatch, cada um com as propriedades file, line, column e content.

const matches = await ctx.grep(file, /console\.error\(/);
for (const match of matches) {
ctx.report.violation({
message: "Use logError() instead of console.error()",
file: match.file,
line: match.line,
});
}

Busca em múltiplos arquivos que correspondem a um padrão glob. Retorna um array plano de objetos GrepMatch de todos os arquivos correspondentes. Arquivos ignorados pelo .gitignore são excluídos por padrão. Defina respectGitignore: false no frontmatter do ADR para incluí-los.

const matches = await ctx.grepFiles(/TODO:/, "src/**/*.ts");
for (const match of matches) {
ctx.report.warning({
message: "TODO comment found",
file: match.file,
line: match.line,
});
}

Encontra arquivos por padrão glob. Retorna um array de caminhos de arquivo relativos à raiz do projeto. Arquivos ignorados pelo .gitignore são excluídos por padrão. Defina respectGitignore: false no frontmatter do ADR para incluí-los.

const testFiles = await ctx.glob("tests/**/*.test.ts");

A interface de relatório com três métodos de severidade:

  • ctx.report.violation(detail) — severidade error (código de saída 1, bloqueia CI)
  • ctx.report.warning(detail) — severidade warning (registrado, mas não bloqueia)
  • ctx.report.info(detail) — informacional (registrado para visibilidade)

Cada método aceita um objeto com:

CampoTipoObrigatórioDescrição
messagestringSimQual é a violação
filestringNãoCaminho do arquivo com a violação
linenumberNãoNúmero da linha da violação
fixstringNãoCorreção sugerida (exibida ao desenvolvedor)
ctx.report.violation({
message: "Command file must export a register*Command function",
file: "src/commands/check.ts",
line: 5,
fix: "Add: export function registerCheckCommand(program: Command) { ... }",
});

O caminho absoluto para o diretório raiz do projeto. Útil quando você precisa construir caminhos absolutos.

Cada regra pode definir uma severidade padrão em sua configuração. A severidade determina como as violações são tratadas:

SeveridadeCódigo de saídaComportamento
error1Bloqueia CI, deve ser corrigido
warning0Registrado, mas não bloqueia
info0Informacional, registrado para visibilidade

Defina a severidade na definição da regra:

export default {
rules: {
"my-rule": {
description: "...",
severity: "warning",
async check(ctx) {
// Violations from this rule are warnings, not errors
ctx.report.violation({ message: "..." });
},
},
},
} satisfies RuleSet;

Se severity for omitido, o padrão é error.

Você também pode reportar com diferentes severidades dentro da mesma regra usando ctx.report.violation(), ctx.report.warning() e ctx.report.info() diretamente.

Cada regra tem um timeout de execução de 30 segundos. Se uma regra exceder esse limite, ela é tratada como erro. Isso impede que verificações descontroladas bloqueiem o pipeline.

Mantenha as regras rápidas:

  • Usando ctx.grepFiles() ao invés de ler cada arquivo manualmente
  • Usando Promise.all() para verificar arquivos em paralelo
  • Delimitando regras com o campo files do frontmatter para limitar o número de arquivos processados

O campo fix é uma string opcional exibida ao desenvolvedor junto com a mensagem de violação. Ele descreve qual ação tomar para resolver o problema. Correções não são aplicadas automaticamente — são orientações.

ctx.report.violation({
message: `Unapproved dependency: "chalk"`,
file: "package.json",
fix: "Use styleText() from node:util instead of chalk",
});

Quando exibido, o fix aparece abaixo da mensagem de violação:

ARCH-006/no-unapproved-deps
package.json
Unapproved dependency: "chalk"
Fix: Use styleText() from node:util instead of chalk
  1. Use Promise.all() para verificações de arquivo em paralelo. Ao verificar múltiplos arquivos independentes, processe-os em paralelo ao invés de sequencialmente.

    // Good: parallel
    const checks = files.map(async (file) => {
    const content = await ctx.readFile(file);
    // ...
    });
    await Promise.all(checks);
    // Avoid: sequential
    for (const file of files) {
    const content = await ctx.readFile(file);
    // ...
    }
  2. Use ctx.changedFiles para verificação incremental. ctx.changedFiles é auto-preenchido com o diff da branch mais as alterações não commitadas da árvore de trabalho (ou arquivos staged com --staged). Filtre ctx.scopedFiles com ele para verificar apenas o que mudou, ou use-o diretamente para regras de dependência entre arquivos.

  3. Mantenha as regras focadas em uma única preocupação. Uma regra que verifica tanto convenções de nomenclatura quanto padrões de import deve ser dividida em duas regras com IDs separados.

  4. Use ctx.grepFiles() ao invés de iteração manual. Ao buscar um padrão em muitos arquivos, ctx.grepFiles() é mais eficiente do que ler cada arquivo e executar uma regex.

  5. Forneça mensagens de fix acionáveis. Um fix como “Não faça isso” não é útil. Diga ao desenvolvedor exatamente o que fazer.

  6. Filtre arquivos não-aplicáveis cedo. Se sua regra se aplica apenas a certos arquivos dentro do escopo, filtre ctx.scopedFiles antes de processar:

    const commandFiles = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts"));
  7. Trate arquivos ausentes com elegância. Se sua regra lê um arquivo específico como package.json, envolva a leitura em um try/catch e retorne antecipadamente se o arquivo não existir.

Existem duas formas de lidar com exceções no Archgate: supressão no nível do engine (funciona com qualquer regra automaticamente) e diretivas customizadas no nível da regra (implementadas pelo autor da regra para opt-outs específicos do domínio).

O Archgate suporta comentários inline archgate-ignore que suprimem violações sem modificar a regra em si. O engine analisa esses comentários e filtra as violações correspondentes antes de reportar.

Supressão da próxima linha: suprime a violação na linha imediatamente seguinte:

// archgate-ignore ARCH-006/no-unapproved-deps dep legada, migração planejada para Q3
import chalk from "chalk";

Supressão no nível do arquivo: suprime todas as violações correspondentes em qualquer lugar do arquivo:

// archgate-ignore-file ARCH-005/test-mirrors-src arquivo gerado, sem teste manual

Múltiplas regras: empilhe comentários para suprimir mais de uma regra na mesma linha:

// archgate-ignore ARCH-006/no-unapproved-deps dep legada
// archgate-ignore ARCH-003/use-style-text lib de terceiros lida com cores
import chalk from "chalk";

Comentários de supressão consecutivos todos visam a primeira linha que não é uma supressão após o bloco.

O formato é ADR-ID/rule-id seguido de um motivo. O motivo é obrigatório: uma supressão sem motivo é ignorada e produz um aviso:

[suppression] Suppression for ARCH-006/no-unapproved-deps is missing a reason src/foo.ts:1

Ambos os estilos de comentário // e # são suportados, então as supressões funcionam em TypeScript, JavaScript, YAML, Python, shell scripts e outros tipos de arquivo que suas regras podem analisar.

Para opt-outs específicos do domínio, autores de regras podem implementar suas próprias diretivas baseadas em comentários dentro da função check. Esse padrão dá à regra controle total sobre a sintaxe, posicionamento e validação da diretiva.

// No seu arquivo .rules.ts:
async check(ctx) {
const files = await ctx.glob("src/components/**/*Connected.tsx");
for (const file of files) {
const content = await ctx.readFile(file);
// Suportar diretiva de opt-out no topo do arquivo
if (/^\/\/\s*@no-presentational:/u.test(content.trimStart())) continue;
// ... lógica da regra que pode reportar uma violação ...
ctx.report.violation({
message: "Componente presentational ausente",
file,
fix: 'Adicione "// @no-presentational: <motivo>" no topo do arquivo para fazer opt-out',
});
}
}

O desenvolvedor faz opt-out adicionando a diretiva ao seu arquivo:

// @no-presentational: este componente apenas redireciona, sem UI para renderizar
import { useNavigate } from "react-router";
AbordagemMelhor paraQuem controla
archgate-ignoreExceções ad-hoc para qualquer regraDesenvolvedor usando a regra
Diretiva customizadaOpt-outs específicos do domínio com motivos estruturadosAutor da regra

Use archgate-ignore quando um desenvolvedor precisa suprimir uma violação pontual. Use diretivas customizadas quando o opt-out é um conceito de primeira classe no domínio da sua regra. Por exemplo, marcar um componente como intencionalmente sem par, ou um arquivo como auto-gerado.

  • Padrões Comuns de Regras: padrões prontos para copiar e colar, organizados por categoria: gerenciamento de dependências, restrições de import, estrutura de arquivos, qualidade de código, esquema de banco de dados e limites de arquitetura.
  • Referência da API de Regras: referência completa de todos os tipos e funções da API de regras.
  • Integração com CI: integre archgate check ao seu pipeline para aplicar regras em cada PR.